wyzeapy 0.5.27__py3-none-any.whl → 0.5.29__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,9 +31,9 @@ class SensorService(BaseService):
31
31
  sensor.device_params = await self.get_updated_params(sensor.mac)
32
32
  properties = await self._get_device_info(sensor)
33
33
 
34
- for property in properties['data']['property_list']:
35
- pid = property['pid']
36
- value = property['value']
34
+ for property in properties["data"]["property_list"]:
35
+ pid = property["pid"]
36
+ value = property["value"]
37
37
 
38
38
  try:
39
39
  if PropertyIDs(pid) == PropertyIDs.CONTACT_STATE:
@@ -45,26 +45,44 @@ class SensorService(BaseService):
45
45
 
46
46
  return sensor
47
47
 
48
- async def register_for_updates(self, sensor: Sensor, callback: Callable[[Sensor], None]):
48
+ async def register_for_updates(
49
+ self, sensor: Sensor, callback: Callable[[Sensor], None]
50
+ ):
49
51
  _LOGGER.debug(f"Registering sensor: {sensor.nickname} for updates")
50
52
  loop = asyncio.get_event_loop()
51
53
  if not self._updater_thread:
52
- self._updater_thread = Thread(target=self.update_worker, args=[loop, ], daemon=True)
54
+ self._updater_thread = Thread(
55
+ target=self.update_worker,
56
+ args=[
57
+ loop,
58
+ ],
59
+ daemon=True,
60
+ )
53
61
  self._updater_thread.start()
54
62
 
55
63
  self._subscribers.append((sensor, callback))
56
64
 
57
65
  async def deregister_for_updates(self, sensor: Sensor):
58
- self._subscribers = [(sense, callback) for sense, callback in self._subscribers if sense.mac != sensor.mac]
66
+ self._subscribers = [
67
+ (sense, callback)
68
+ for sense, callback in self._subscribers
69
+ if sense.mac != sensor.mac
70
+ ]
59
71
 
60
72
  def update_worker(self, loop):
61
73
  while True:
62
74
  for sensor, callback in self._subscribers:
63
75
  _LOGGER.debug(f"Providing update for {sensor.nickname}")
64
76
  try:
65
- callback(asyncio.run_coroutine_threadsafe(self.update(sensor), loop).result())
77
+ callback(
78
+ asyncio.run_coroutine_threadsafe(
79
+ self.update(sensor), loop
80
+ ).result()
81
+ )
66
82
  except UnknownApiError as e:
67
- _LOGGER.warning(f"The update method detected an UnknownApiError: {e}")
83
+ _LOGGER.warning(
84
+ f"The update method detected an UnknownApiError: {e}"
85
+ )
68
86
  except ClientOSError as e:
69
87
  _LOGGER.error(f"A network error was detected: {e}")
70
88
  except ContentTypeError as e:
@@ -74,7 +92,10 @@ class SensorService(BaseService):
74
92
  if self._devices is None:
75
93
  self._devices = await self.get_object_list()
76
94
 
77
- sensors = [Sensor(device.raw_dict) for device in self._devices if
78
- device.type is DeviceTypes.MOTION_SENSOR or
79
- device.type is DeviceTypes.CONTACT_SENSOR]
95
+ sensors = [
96
+ Sensor(device.raw_dict)
97
+ for device in self._devices
98
+ if device.type is DeviceTypes.MOTION_SENSOR
99
+ or device.type is DeviceTypes.CONTACT_SENSOR
100
+ ]
80
101
  return [Sensor(sensor.raw_dict) for sensor in sensors]
@@ -36,8 +36,12 @@ class SwitchService(BaseService):
36
36
  if self._devices is None:
37
37
  self._devices = await self.get_object_list()
38
38
 
39
- devices = [device for device in self._devices if device.type is DeviceTypes.PLUG or
40
- device.type is DeviceTypes.OUTDOOR_PLUG]
39
+ devices = [
40
+ device
41
+ for device in self._devices
42
+ if device.type is DeviceTypes.PLUG
43
+ or device.type is DeviceTypes.OUTDOOR_PLUG
44
+ ]
41
45
  return [Switch(switch.raw_dict) for switch in devices]
42
46
 
43
47
  async def turn_on(self, switch: Switch):
@@ -37,9 +37,9 @@ class Preset(Enum):
37
37
 
38
38
 
39
39
  class HVACState(Enum):
40
- COOLING = 'cooling'
41
- HEATING = 'heating'
42
- IDLE = 'idle'
40
+ COOLING = "cooling"
41
+ HEATING = "heating"
42
+ IDLE = "idle"
43
43
 
44
44
 
45
45
  class Thermostat(Device):
@@ -60,7 +60,7 @@ class Thermostat(Device):
60
60
 
61
61
  class ThermostatService(BaseService):
62
62
  async def update(self, thermostat: Thermostat) -> Thermostat:
63
- properties = (await self._thermostat_get_iot_prop(thermostat))['data']['props']
63
+ properties = (await self._thermostat_get_iot_prop(thermostat))["data"]["props"]
64
64
 
65
65
  device_props = []
66
66
  for property in properties:
@@ -87,7 +87,7 @@ class ThermostatService(BaseService):
87
87
  elif prop == ThermostatProps.TEMPERATURE:
88
88
  thermostat.temperature = float(value)
89
89
  elif prop == ThermostatProps.IOT_STATE:
90
- thermostat.available = value == 'connected'
90
+ thermostat.available = value == "connected"
91
91
  elif prop == ThermostatProps.HUMIDITY:
92
92
  thermostat.humidity = int(value)
93
93
  elif prop == ThermostatProps.WORKING_STATE:
@@ -99,7 +99,9 @@ class ThermostatService(BaseService):
99
99
  if self._devices is None:
100
100
  self._devices = await self.get_object_list()
101
101
 
102
- thermostats = [device for device in self._devices if device.type is DeviceTypes.THERMOSTAT]
102
+ thermostats = [
103
+ device for device in self._devices if device.type is DeviceTypes.THERMOSTAT
104
+ ]
103
105
 
104
106
  return [Thermostat(thermostat.raw_dict) for thermostat in thermostats]
105
107
 
@@ -110,22 +112,34 @@ class ThermostatService(BaseService):
110
112
  await self._thermostat_set_iot_prop(thermostat, ThermostatProps.HEAT_SP, temp)
111
113
 
112
114
  async def set_hvac_mode(self, thermostat: Device, hvac_mode: HVACMode):
113
- await self._thermostat_set_iot_prop(thermostat, ThermostatProps.MODE_SYS, hvac_mode.value)
115
+ await self._thermostat_set_iot_prop(
116
+ thermostat, ThermostatProps.MODE_SYS, hvac_mode.value
117
+ )
114
118
 
115
119
  async def set_fan_mode(self, thermostat: Device, fan_mode: FanMode):
116
- await self._thermostat_set_iot_prop(thermostat, ThermostatProps.FAN_MODE, fan_mode.value)
120
+ await self._thermostat_set_iot_prop(
121
+ thermostat, ThermostatProps.FAN_MODE, fan_mode.value
122
+ )
117
123
 
118
124
  async def set_preset(self, thermostat: Thermostat, preset: Preset):
119
- await self._thermostat_set_iot_prop(thermostat, ThermostatProps.CURRENT_SCENARIO, preset.value)
125
+ await self._thermostat_set_iot_prop(
126
+ thermostat, ThermostatProps.CURRENT_SCENARIO, preset.value
127
+ )
120
128
 
121
129
  async def _thermostat_get_iot_prop(self, device: Device) -> Dict[Any, Any]:
122
130
  url = "https://wyze-earth-service.wyzecam.com/plugin/earth/get_iot_prop"
123
- keys = 'trigger_off_val,emheat,temperature,humidity,time2temp_val,protect_time,mode_sys,heat_sp,cool_sp,' \
124
- 'current_scenario,config_scenario,temp_unit,fan_mode,iot_state,w_city_id,w_lat,w_lon,working_state,' \
125
- 'dev_hold,dev_holdtime,asw_hold,app_version,setup_state,wiring_logic_id,save_comfort_balance,' \
126
- 'kid_lock,calibrate_humidity,calibrate_temperature,fancirc_time,query_schedule'
131
+ keys = (
132
+ "trigger_off_val,emheat,temperature,humidity,time2temp_val,protect_time,mode_sys,heat_sp,cool_sp,"
133
+ "current_scenario,config_scenario,temp_unit,fan_mode,iot_state,w_city_id,w_lat,w_lon,working_state,"
134
+ "dev_hold,dev_holdtime,asw_hold,app_version,setup_state,wiring_logic_id,save_comfort_balance,"
135
+ "kid_lock,calibrate_humidity,calibrate_temperature,fancirc_time,query_schedule"
136
+ )
127
137
  return await self._get_iot_prop(url, device, keys)
128
138
 
129
- async def _thermostat_set_iot_prop(self, device: Device, prop: ThermostatProps, value: Any) -> None:
130
- url = "https://wyze-earth-service.wyzecam.com/plugin/earth/set_iot_prop_by_topic"
139
+ async def _thermostat_set_iot_prop(
140
+ self, device: Device, prop: ThermostatProps, value: Any
141
+ ) -> None:
142
+ url = (
143
+ "https://wyze-earth-service.wyzecam.com/plugin/earth/set_iot_prop_by_topic"
144
+ )
131
145
  return await self._set_iot_prop(url, device, prop.value, value)
@@ -7,17 +7,34 @@ from ..types import Device
7
7
  import logging
8
8
  import threading
9
9
 
10
+ """
11
+ Asynchronous device update scheduling and management.
12
+
13
+ This module provides classes to schedule and execute periodic updates of
14
+ Wyze devices, ensuring rate limits and fair distribution of update calls.
15
+ """
16
+
10
17
  _LOGGER = logging.getLogger(__name__)
11
18
 
12
19
  INTERVAL = 300
13
20
  MAX_SLOTS = 225
14
21
 
22
+
15
23
  @dataclass(order=True)
16
24
  class 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
25
+ """Represents a scheduled update task for a single device.
26
+
27
+ Attributes:
28
+ service: The service instance responsible for updating the device.
29
+ device: The Device object to be updated.
30
+ update_in: Countdown ticks until the next update is due.
31
+ updates_per_interval: Number of updates allowed per INTERVAL.
32
+ """
33
+
34
+ device: Device = field(compare=False)
35
+ service: Any = field(compare=False)
36
+ update_in: int # Countdown ticks until this device should be updated
37
+ updates_per_interval: int = field(compare=False)
21
38
 
22
39
  def __init__(self, service, device: Device, update_interval: int):
23
40
  """
@@ -42,7 +59,7 @@ class DeviceUpdater(object):
42
59
  self.device = await self.service.update(self.device)
43
60
  # Callback to provide the updated info to the subscriber
44
61
  self.device.callback_function(self.device)
45
- except:
62
+ except Exception:
46
63
  _LOGGER.exception("Unknown error happened during updating device info")
47
64
  finally:
48
65
  # Release the mutex after the async call
@@ -63,11 +80,17 @@ class DeviceUpdater(object):
63
80
  if self.updates_per_interval > 1:
64
81
  self.updates_per_interval -= 1
65
82
 
83
+
66
84
  class UpdateManager:
67
- # Holds all the logic for when to update the devices
85
+ """Manager for scheduling and executing periodic device updates.
86
+
87
+ Maintains a priority queue of DeviceUpdater instances and enforces rate
88
+ limits and fair distribution of update calls across devices.
89
+ """
90
+
68
91
  updaters = []
69
92
  removed_updaters = []
70
- mutex = threading.Lock() # Create a lock object as a class variable
93
+ mutex = threading.Lock()
71
94
 
72
95
  def check_if_removed(self, updater: DeviceUpdater):
73
96
  for item in self.removed_updaters:
@@ -78,7 +101,7 @@ class UpdateManager:
78
101
  # This function should be called once every second
79
102
  async def update_next(self):
80
103
  # If there are no updaters in the queue we don't need to do anything
81
- if (len(self.updaters) == 0):
104
+ if len(self.updaters) == 0:
82
105
  _LOGGER.debug("No devices to update in queue")
83
106
  return
84
107
  while True:
@@ -92,12 +115,13 @@ class UpdateManager:
92
115
  # We then reduce the counter for all the other updaters
93
116
  self.tick_tock()
94
117
  # 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.
118
+ await updater.update(
119
+ self.mutex
120
+ ) # It will only update if it is time for it to update. Otherwise it just reduces its update_in counter.
96
121
  # Then we put it back at the end of the queue. Or the front again if it wasn't ready to update
97
122
  heappush(self.updaters, updater)
98
123
  await sleep(1)
99
124
 
100
-
101
125
  def filled_slots(self):
102
126
  # This just returns the number of available slots
103
127
  current_slots = 0
@@ -123,7 +147,10 @@ class UpdateManager:
123
147
 
124
148
  # When we add a new updater it has to fit within the max slots or we will not add it
125
149
  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())
150
+ _LOGGER.debug(
151
+ "Reducing updates per interval to fit new device as slots are full: %s",
152
+ self.filled_slots(),
153
+ )
127
154
  # 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
155
  self.decrease_updates_per_interval()
129
156
  updater.delay()
@@ -41,7 +41,7 @@ class WallSwitch(Device):
41
41
 
42
42
  class WallSwitchService(BaseService):
43
43
  async def update(self, switch: WallSwitch) -> WallSwitch:
44
- properties = (await self._wall_switch_get_iot_prop(switch))['data']['props']
44
+ properties = (await self._wall_switch_get_iot_prop(switch))["data"]["props"]
45
45
 
46
46
  device_props = []
47
47
  for prop_key, prop_value in properties.items():
@@ -67,9 +67,11 @@ class WallSwitchService(BaseService):
67
67
  if self._devices is None:
68
68
  self._devices = await self.get_object_list()
69
69
 
70
- switches = [device for device in self._devices
71
- if device.type is DeviceTypes.COMMON
72
- and device.product_model == "LD_SS1"]
70
+ switches = [
71
+ device
72
+ for device in self._devices
73
+ if device.type is DeviceTypes.COMMON and device.product_model == "LD_SS1"
74
+ ]
73
75
 
74
76
  return [WallSwitch(switch.raw_dict) for switch in switches]
75
77
 
@@ -89,7 +91,9 @@ class WallSwitchService(BaseService):
89
91
  await self._wall_switch_set_iot_prop(switch, WallSwitchProps.SWITCH_POWER, True)
90
92
 
91
93
  async def power_off(self, switch: WallSwitch):
92
- await self._wall_switch_set_iot_prop(switch, WallSwitchProps.SWITCH_POWER, False)
94
+ await self._wall_switch_set_iot_prop(
95
+ switch, WallSwitchProps.SWITCH_POWER, False
96
+ )
93
97
 
94
98
  async def iot_on(self, switch: WallSwitch):
95
99
  await self._wall_switch_set_iot_prop(switch, WallSwitchProps.SWITCH_IOT, True)
@@ -97,14 +101,20 @@ class WallSwitchService(BaseService):
97
101
  async def iot_off(self, switch: WallSwitch):
98
102
  await self._wall_switch_set_iot_prop(switch, WallSwitchProps.SWITCH_IOT, False)
99
103
 
100
- async def set_single_press_type(self, switch: WallSwitch, single_press_type: SinglePressType):
101
- await self._wall_switch_set_iot_prop(switch, WallSwitchProps.SINGLE_PRESS_TYPE, single_press_type.value)
104
+ async def set_single_press_type(
105
+ self, switch: WallSwitch, single_press_type: SinglePressType
106
+ ):
107
+ await self._wall_switch_set_iot_prop(
108
+ switch, WallSwitchProps.SINGLE_PRESS_TYPE, single_press_type.value
109
+ )
102
110
 
103
111
  async def _wall_switch_get_iot_prop(self, device: Device) -> Dict[Any, Any]:
104
112
  url = "https://wyze-sirius-service.wyzecam.com//plugin/sirius/get_iot_prop"
105
113
  keys = "iot_state,switch-power,switch-iot,single_press_type"
106
114
  return await self._get_iot_prop(url, device, keys)
107
115
 
108
- async def _wall_switch_set_iot_prop(self, device: Device, prop: WallSwitchProps, value: Any) -> None:
116
+ async def _wall_switch_set_iot_prop(
117
+ self, device: Device, prop: WallSwitchProps, value: Any
118
+ ) -> None:
109
119
  url = "https://wyze-sirius-service.wyzecam.com//plugin/sirius/set_iot_prop_by_topic"
110
120
  return await self._set_iot_prop(url, device, prop.value, value)
wyzeapy/types.py CHANGED
@@ -3,6 +3,11 @@
3
3
  # of the attached license. You should have received a copy of
4
4
  # the license with this file. If not, please write to:
5
5
  # katie@mulliken.net to receive a copy
6
+ """
7
+ Type definitions and data models for Wyzeapy library, including devices, events,
8
+ and API response code enums.
9
+ """
10
+
6
11
  from enum import Enum
7
12
  from typing import Union, List, Dict, Any
8
13
 
@@ -79,15 +84,17 @@ class Sensor(Device):
79
84
  @property
80
85
  def activity_detected(self) -> int:
81
86
  if self.type is DeviceTypes.CONTACT_SENSOR:
82
- return int(self.device_params['open_close_state'])
87
+ return int(self.device_params["open_close_state"])
83
88
  elif self.type is DeviceTypes.MOTION_SENSOR:
84
- return int(self.device_params['motion_state'])
89
+ return int(self.device_params["motion_state"])
85
90
  else:
86
- raise AssertionError("Device must be of type CONTACT_SENSOR or MOTION_SENSOR")
91
+ raise AssertionError(
92
+ "Device must be of type CONTACT_SENSOR or MOTION_SENSOR"
93
+ )
87
94
 
88
95
  @property
89
96
  def is_low_battery(self) -> int:
90
- return int(self.device_params['is_low_battery'])
97
+ return int(self.device_params["is_low_battery"])
91
98
 
92
99
 
93
100
  class PropertyIDs(Enum):
@@ -104,11 +111,11 @@ class PropertyIDs(Enum):
104
111
  CONTACT_STATE = "P1301"
105
112
  MOTION_STATE = "P1302"
106
113
  CAMERA_SIREN = "P1049"
107
- ACCESSORY = "P1056" # Is state for camera accessories, like garage doors, light sockets, and floodlights.
114
+ ACCESSORY = "P1056" # Is state for camera accessories, like garage doors, light sockets, and floodlights.
108
115
  SUN_MATCH = "P1528"
109
116
  MOTION_DETECTION = "P1047" # Current Motion Detection State of the Camera
110
117
  MOTION_DETECTION_TOGGLE = "P1001" # This toggles Camera Motion Detection On/Off
111
- WCO_MOTION_DETECTION = "P1029" # Wyze cam outdoor requires both P1047 and P1029 to be set. P1029 is set via set_property_list
118
+ WCO_MOTION_DETECTION = "P1029" # Wyze cam outdoor requires both P1047 and P1029 to be set. P1029 is set via set_property_list
112
119
 
113
120
 
114
121
  class WallSwitchProps(Enum):
@@ -153,7 +160,7 @@ class ResponseCodes(Enum):
153
160
  SUCCESS = "1"
154
161
  PARAMETER_ERROR = "1001"
155
162
  ACCESS_TOKEN_ERROR = "2001"
156
- DEVICE_OFFLINE = '3019'
163
+ DEVICE_OFFLINE = "3019"
157
164
 
158
165
 
159
166
  class ResponseCodesLock(Enum):
@@ -205,9 +212,9 @@ class Event:
205
212
 
206
213
 
207
214
  class HMSStatus(Enum):
208
- DISARMED = 'disarmed'
209
- HOME = 'home'
210
- AWAY = 'away'
215
+ DISARMED = "disarmed"
216
+ HOME = "home"
217
+ AWAY = "away"
211
218
 
212
219
 
213
220
  class DeviceMgmtToggleType:
@@ -217,6 +224,7 @@ class DeviceMgmtToggleType:
217
224
 
218
225
 
219
226
  class DeviceMgmtToggleProps(Enum):
220
- EVENT_RECORDING_TOGGLE = DeviceMgmtToggleType("cam_event_recording", "ge.motion_detect_recording")
227
+ EVENT_RECORDING_TOGGLE = DeviceMgmtToggleType(
228
+ "cam_event_recording", "ge.motion_detect_recording"
229
+ )
221
230
  NOTIFICATION_TOGGLE = DeviceMgmtToggleType("cam_device_notify", "ge.push_switch")
222
-
wyzeapy/utils.py CHANGED
@@ -4,6 +4,7 @@
4
4
  # the license with this file. If not, please write to:
5
5
  # katie@mulliken.net to receive a copy
6
6
  import base64
7
+ import binascii
7
8
  import hashlib
8
9
  from typing import Dict, Any, List, Optional
9
10
 
@@ -12,6 +13,10 @@ from Crypto.Cipher import AES
12
13
  from .exceptions import ParameterError, AccessTokenError, UnknownApiError
13
14
  from .types import ResponseCodes, PropertyIDs, Device, Event
14
15
 
16
+ """
17
+ Utility functions for encryption, decryption, error handling, and common Wyze API tasks.
18
+ """
19
+
15
20
  PADDING = bytes.fromhex("05")
16
21
 
17
22
 
@@ -43,7 +48,7 @@ def wyze_encrypt(key, text):
43
48
  cipher = AES.new(key, AES.MODE_CBC, iv)
44
49
  enc = cipher.encrypt(raw)
45
50
  b64_enc = base64.b64encode(enc).decode("ascii")
46
- b64_enc = b64_enc.replace("/", r'\/')
51
+ b64_enc = b64_enc.replace("/", r"\/")
47
52
  return b64_enc
48
53
 
49
54
 
@@ -56,7 +61,7 @@ def wyze_decrypt(key, enc):
56
61
  """
57
62
  enc = base64.b64decode(enc)
58
63
 
59
- key = key.encode('ascii')
64
+ key = key.encode("ascii")
60
65
  iv = key
61
66
  cipher = AES.new(key, AES.MODE_CBC, iv)
62
67
  decrypt = cipher.decrypt(enc)
@@ -66,31 +71,80 @@ def wyze_decrypt(key, enc):
66
71
  return decrypt_txt
67
72
 
68
73
 
74
+ def wyze_decrypt_cbc(key: str, enc_hex_str: str) -> str:
75
+ """
76
+ Decrypt a hex-encoded string using Wyze's CBC decryption with MD5 based key.
77
+
78
+ Args:
79
+ key: The secret key string.
80
+ enc_hex_str: The encrypted data as a hex string.
81
+
82
+ Returns:
83
+ The decrypted plaintext string.
84
+ """
85
+ key_hash = hashlib.md5(key.encode("utf-8")).digest()
86
+
87
+ iv = b"0123456789ABCDEF"
88
+ cipher = AES.new(key_hash, AES.MODE_CBC, iv)
89
+
90
+ encrypted_bytes = binascii.unhexlify(enc_hex_str)
91
+ decrypted_bytes = cipher.decrypt(encrypted_bytes)
92
+
93
+ # PKCS5Padding
94
+ padding_length = decrypted_bytes[-1]
95
+ return decrypted_bytes[:-padding_length].decode()
96
+
97
+
69
98
  def create_password(password: str) -> str:
99
+ """
100
+ Derive the Wyze API password hash using a triple MD5 hashing scheme.
101
+
102
+ Args:
103
+ password: The plain user password string.
104
+
105
+ Returns:
106
+ The hashed password as a hex string.
107
+ """
70
108
  hex1 = hashlib.md5(password.encode()).hexdigest()
71
109
  hex2 = hashlib.md5(hex1.encode()).hexdigest()
72
110
  return hashlib.md5(hex2.encode()).hexdigest()
73
111
 
74
112
 
75
113
  def check_for_errors_standard(service, response_json: Dict[str, Any]) -> None:
76
- response_code = response_json['code']
114
+ """
115
+ Check for standard Wyze API error codes and raise exceptions as needed.
116
+
117
+ Args:
118
+ service: The service instance triggering the call.
119
+ response_json: The JSON response from the API.
120
+ """
121
+ response_code = response_json["code"]
77
122
  if response_code != ResponseCodes.SUCCESS.value:
78
123
  if response_code == ResponseCodes.PARAMETER_ERROR.value:
79
- raise ParameterError(response_code, response_json['msg'])
124
+ raise ParameterError(response_code, response_json["msg"])
80
125
  elif response_code == ResponseCodes.ACCESS_TOKEN_ERROR.value:
81
126
  service._auth_lib.token.expired = True
82
- raise AccessTokenError(response_code, "Access Token expired, attempting to refresh")
127
+ raise AccessTokenError(
128
+ response_code, "Access Token expired, attempting to refresh"
129
+ )
83
130
  elif response_code == ResponseCodes.DEVICE_OFFLINE.value:
84
131
  return
85
132
  else:
86
- raise UnknownApiError(response_code, response_json['msg'])
133
+ raise UnknownApiError(response_code, response_json["msg"])
87
134
 
88
135
 
89
136
  def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None:
90
- if response_json['ErrNo'] != 0:
91
- if response_json.get('code') == ResponseCodes.PARAMETER_ERROR.value:
137
+ """
138
+ Check for lock-specific API errors and raise exceptions as needed.
139
+
140
+ Args:
141
+ service: The lock service instance.
142
+ response_json: The JSON response from the lock API.
143
+ """
144
+ if response_json["ErrNo"] != 0:
145
+ if response_json.get("code") == ResponseCodes.PARAMETER_ERROR.value:
92
146
  raise ParameterError(response_json)
93
- elif response_json.get('code') == ResponseCodes.ACCESS_TOKEN_ERROR.value:
147
+ elif response_json.get("code") == ResponseCodes.ACCESS_TOKEN_ERROR.value:
94
148
  service._auth_lib.token.expired = True
95
149
  raise AccessTokenError("Access Token expired, attempting to refresh")
96
150
  else:
@@ -98,8 +152,15 @@ def check_for_errors_lock(service, response_json: Dict[str, Any]) -> None:
98
152
 
99
153
 
100
154
  def check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None:
101
- if response_json['status'] != 200:
102
- if "InvalidTokenError>" in response_json['response']['errors'][0]['message']:
155
+ """
156
+ Check for device management API errors and raise exceptions as needed.
157
+
158
+ Args:
159
+ service: The device management service instance.
160
+ response_json: The JSON response from the device management API.
161
+ """
162
+ if response_json["status"] != 200:
163
+ if "InvalidTokenError>" in response_json["response"]["errors"][0]["message"]:
103
164
  service._auth_lib.token.expired = True
104
165
  raise AccessTokenError("Access Token expired, attempting to refresh")
105
166
  else:
@@ -107,20 +168,45 @@ def check_for_errors_devicemgmt(service, response_json: Dict[Any, Any]) -> None:
107
168
 
108
169
 
109
170
  def check_for_errors_iot(service, response_json: Dict[Any, Any]) -> None:
110
- if response_json['code'] != 1:
111
- if str(response_json['code']) == ResponseCodes.ACCESS_TOKEN_ERROR.value:
171
+ """
172
+ Check for IoT API errors and raise exceptions as needed.
173
+
174
+ Args:
175
+ service: The IoT service instance.
176
+ response_json: The JSON response from the IoT API.
177
+ """
178
+ if response_json["code"] != 1:
179
+ if str(response_json["code"]) == ResponseCodes.ACCESS_TOKEN_ERROR.value:
112
180
  service._auth_lib.token.expired = True
113
181
  raise AccessTokenError("Access Token expired, attempting to refresh")
114
182
  else:
115
183
  raise UnknownApiError(response_json)
116
184
 
185
+
117
186
  def check_for_errors_hms(service, response_json: Dict[Any, Any]) -> None:
118
- if response_json['message'] is None:
187
+ """
188
+ Check for home monitoring system (HMS) API errors and raise exceptions as needed.
189
+
190
+ Args:
191
+ service: The HMS service instance.
192
+ response_json: The JSON response from the HMS API.
193
+ """
194
+ if response_json["message"] is None:
119
195
  service._auth_lib.token.expired = True
120
196
  raise AccessTokenError("Access Token expired, attempting to refresh")
121
197
 
122
198
 
123
199
  def return_event_for_device(device: Device, events: List[Event]) -> Optional[Event]:
200
+ """
201
+ Retrieve the most recent event matching a given device from a list of events.
202
+
203
+ Args:
204
+ device: The device to match against event.device_mac.
205
+ events: List of events to search.
206
+
207
+ Returns:
208
+ The first matching Event or None if no match is found.
209
+ """
124
210
  for event in events:
125
211
  if event.device_mac == device.mac:
126
212
  return event
@@ -129,4 +215,14 @@ def return_event_for_device(device: Device, events: List[Event]) -> Optional[Eve
129
215
 
130
216
 
131
217
  def create_pid_pair(pid_enum: PropertyIDs, value: str) -> Dict[str, str]:
218
+ """
219
+ Create a property ID/value pair dictionary for API payloads.
220
+
221
+ Args:
222
+ pid_enum: PropertyIDs enum member for the property.
223
+ value: The value to set for the property.
224
+
225
+ Returns:
226
+ A dict with 'pid' and 'pvalue' keys for the Wyze API.
227
+ """
132
228
  return {"pid": pid_enum.value, "pvalue": value}