wyzeapy 0.5.26__py3-none-any.whl → 0.5.27__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.
@@ -29,7 +29,7 @@ class BaseService:
29
29
  _devices: Optional[List[Device]] = None
30
30
  _last_updated_time: time = 0 # preload a value of 0 so that comparison will succeed on the first run
31
31
  _min_update_time = 1200 # lets let the device_params update every 20 minutes for now. This could probably reduced signicficantly.
32
- _update_lock: asyncio.Lock() = asyncio.Lock()
32
+ _update_lock: asyncio.Lock = asyncio.Lock() # fmt: skip
33
33
  _update_manager: UpdateManager = UpdateManager()
34
34
  _update_loop = None
35
35
  _updater: DeviceUpdater = None
@@ -120,7 +120,6 @@ class BaseService:
120
120
  json=payload)
121
121
 
122
122
  check_for_errors_standard(self, response_json)
123
-
124
123
  # Cache the devices so that update calls can pull more recent device_params
125
124
  BaseService._devices = [Device(device) for device in response_json['data']['device_list']]
126
125
 
@@ -166,7 +165,6 @@ class BaseService:
166
165
 
167
166
  check_for_errors_standard(self, response_json)
168
167
  properties = response_json['data']['property_list']
169
-
170
168
  property_list = []
171
169
  for prop in properties:
172
170
  try:
@@ -32,6 +32,7 @@ class Camera(Device):
32
32
  self.on: bool = True
33
33
  self.siren: bool = False
34
34
  self.floodlight: bool = False
35
+ self.garage: bool = False
35
36
 
36
37
 
37
38
  class CameraService(BaseService):
@@ -76,8 +77,10 @@ class CameraService(BaseService):
76
77
  camera.on = value == "1"
77
78
  if property is PropertyIDs.CAMERA_SIREN:
78
79
  camera.siren = value == "1"
79
- if property is PropertyIDs.FLOOD_LIGHT:
80
+ if property is PropertyIDs.ACCESSORY:
80
81
  camera.floodlight = value == "1"
82
+ if camera.device_params["dongle_product_model"] == "HL_CGDC":
83
+ camera.garage = value == "1" # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app
81
84
  if property is PropertyIDs.NOTIFICATION:
82
85
  camera.notify = value == "1"
83
86
  if property is PropertyIDs.MOTION_DETECTION:
@@ -139,14 +142,21 @@ class CameraService(BaseService):
139
142
  async def floodlight_on(self, camera: Camera):
140
143
  if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "1") # Battery cam pro integrated spotlight is controllable
141
144
  elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "1") # Some camera models use a diffrent api
142
- else: await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "1")
145
+ else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "1")
143
146
 
144
147
  # Also controls lamp socket and BCP spotlight
145
148
  async def floodlight_off(self, camera: Camera):
146
149
  if (camera.product_model == "AN_RSCW"): await self._run_action_devicemgmt(camera, "spotlight", "0") # Battery cam pro integrated spotlight is controllable
147
150
  elif (camera.product_model in DEVICEMGMT_API_MODELS): await self._run_action_devicemgmt(camera, "floodlight", "0") # Some camera models use a diffrent api
148
- else: await self._set_property(camera, PropertyIDs.FLOOD_LIGHT.value, "2")
149
-
151
+ else: await self._set_property(camera, PropertyIDs.ACCESSORY.value, "2")
152
+
153
+ # Garage door trigger uses run action on all models
154
+ async def garage_door_open(self, camera: Camera):
155
+ await self._run_action(camera, "garage_door_trigger")
156
+
157
+ async def garage_door_close(self, camera: Camera):
158
+ await self._run_action(camera, "garage_door_trigger")
159
+
150
160
  async def turn_on_notifications(self, camera: Camera):
151
161
  if (camera.product_model in DEVICEMGMT_API_MODELS): await self._set_toggle(camera, DeviceMgmtToggleProps.NOTIFICATION_TOGGLE.value, "1")
152
162
  else: await self._set_property(camera, PropertyIDs.NOTIFICATION.value, "1")
@@ -0,0 +1,135 @@
1
+ import unittest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from wyzeapy.services.bulb_service import BulbService, Bulb
4
+ from wyzeapy.types import DeviceTypes, PropertyIDs
5
+
6
+
7
+ class TestBulbService(unittest.IsolatedAsyncioTestCase):
8
+ async def asyncSetUp(self):
9
+ mock_auth_lib = MagicMock()
10
+ self.bulb_service = BulbService(auth_lib=mock_auth_lib)
11
+ self.bulb_service._get_property_list = AsyncMock()
12
+ self.bulb_service.get_updated_params = AsyncMock()
13
+
14
+ async def test_update_bulb_basic_properties(self):
15
+ mock_bulb = Bulb({
16
+ "device_type": "Light",
17
+ "product_model": "WLPA19",
18
+ "mac": "TEST123",
19
+ "raw_dict": {},
20
+ "device_params": {"ip": "192.168.1.100"},
21
+ "prop_map": {},
22
+ 'product_type': DeviceTypes.MESH_LIGHT.value
23
+ })
24
+
25
+ # Mock the property list response
26
+ self.bulb_service._get_property_list.return_value = [
27
+ (PropertyIDs.BRIGHTNESS, "75"),
28
+ (PropertyIDs.COLOR_TEMP, "4000"),
29
+ (PropertyIDs.ON, "1"),
30
+ (PropertyIDs.AVAILABLE, "1")
31
+ ]
32
+
33
+ updated_bulb = await self.bulb_service.update(mock_bulb)
34
+
35
+ self.assertEqual(updated_bulb.brightness, 75)
36
+ self.assertEqual(updated_bulb.color_temp, 4000)
37
+ self.assertTrue(updated_bulb.on)
38
+ self.assertTrue(updated_bulb.available)
39
+
40
+ async def test_update_bulb_lightstrip_properties(self):
41
+ mock_bulb = Bulb({
42
+ "device_type": "Light",
43
+ "product_model": "WLST19",
44
+ "mac": "TEST456",
45
+ "raw_dict": {},
46
+ "device_params": {"ip": "192.168.1.101"},
47
+ "prop_map": {},
48
+ 'product_type': DeviceTypes.LIGHTSTRIP.value
49
+ })
50
+ mock_bulb.product_type = DeviceTypes.LIGHTSTRIP
51
+
52
+ # Mock the property list response with the corrected color format (no # symbol)
53
+ self.bulb_service._get_property_list.return_value = [
54
+ (PropertyIDs.COLOR, "FF0000"), # Removed the # symbol
55
+ (PropertyIDs.COLOR_MODE, "1"),
56
+ (PropertyIDs.LIGHTSTRIP_EFFECTS, "rainbow"),
57
+ (PropertyIDs.LIGHTSTRIP_MUSIC_MODE, "1"),
58
+ (PropertyIDs.ON, "1"),
59
+ (PropertyIDs.AVAILABLE, "1")
60
+ ]
61
+
62
+ updated_bulb = await self.bulb_service.update(mock_bulb)
63
+
64
+ self.assertEqual(updated_bulb.color, "FF0000")
65
+ self.assertEqual(updated_bulb.color_mode, "1")
66
+ self.assertEqual(updated_bulb.effects, "rainbow")
67
+ self.assertTrue(updated_bulb.music_mode)
68
+ self.assertTrue(updated_bulb.on)
69
+ self.assertTrue(updated_bulb.available)
70
+
71
+ async def test_update_bulb_sun_match(self):
72
+ mock_bulb = Bulb({
73
+ "device_type": "Light",
74
+ "product_model": "WLPA19",
75
+ "mac": "TEST789",
76
+ "raw_dict": {},
77
+ "device_params": {"ip": "192.168.1.102"},
78
+ "prop_map": {},
79
+ 'product_type': DeviceTypes.MESH_LIGHT.value
80
+ })
81
+
82
+ # Mock the property list response
83
+ self.bulb_service._get_property_list.return_value = [
84
+ (PropertyIDs.SUN_MATCH, "1"),
85
+ (PropertyIDs.ON, "1"),
86
+ (PropertyIDs.AVAILABLE, "1")
87
+ ]
88
+
89
+ updated_bulb = await self.bulb_service.update(mock_bulb)
90
+
91
+ self.assertTrue(updated_bulb.sun_match)
92
+ self.assertTrue(updated_bulb.on)
93
+ self.assertTrue(updated_bulb.available)
94
+
95
+ async def test_update_bulb_invalid_color_temp(self):
96
+ mock_bulb = Bulb({
97
+ "device_type": "Light",
98
+ "product_model": "WLPA19",
99
+ "mac": "TEST101",
100
+ "raw_dict": {},
101
+ "device_params": {"ip": "192.168.1.103"},
102
+ "prop_map": {},
103
+ 'product_type': DeviceTypes.MESH_LIGHT.value
104
+ })
105
+
106
+ # Mock the property list response with invalid color temp
107
+ self.bulb_service._get_property_list.return_value = [
108
+ (PropertyIDs.COLOR_TEMP, "invalid"),
109
+ (PropertyIDs.ON, "1")
110
+ ]
111
+
112
+ updated_bulb = await self.bulb_service.update(mock_bulb)
113
+
114
+ # Should default to 2700K when invalid
115
+ self.assertEqual(updated_bulb.color_temp, 2700)
116
+ self.assertTrue(updated_bulb.on)
117
+
118
+ async def test_get_bulbs(self):
119
+ mock_device = MagicMock()
120
+ mock_device.type = DeviceTypes.LIGHT
121
+ mock_device.raw_dict = {
122
+ "device_type": "Light",
123
+ "product_model": "WLPA19",
124
+ "device_params": {"ip": "192.168.1.104"},
125
+ "prop_map": {},
126
+ 'product_type': DeviceTypes.MESH_LIGHT.value
127
+ }
128
+
129
+ self.bulb_service.get_object_list = AsyncMock(return_value=[mock_device])
130
+
131
+ bulbs = await self.bulb_service.get_bulbs()
132
+
133
+ self.assertEqual(len(bulbs), 1)
134
+ self.assertIsInstance(bulbs[0], Bulb)
135
+ self.bulb_service.get_object_list.assert_awaited_once()
@@ -0,0 +1,180 @@
1
+ import unittest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from wyzeapy.services.camera_service import CameraService, Camera, DEVICEMGMT_API_MODELS
4
+ from wyzeapy.types import DeviceTypes, PropertyIDs, Event
5
+ from wyzeapy.wyze_auth_lib import WyzeAuthLib
6
+
7
+
8
+ class TestCameraService(unittest.IsolatedAsyncioTestCase):
9
+ async def asyncSetUp(self):
10
+ self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
11
+ self.camera_service = CameraService(auth_lib=self.mock_auth_lib)
12
+ self.camera_service._get_property_list = AsyncMock()
13
+ self.camera_service._get_event_list = AsyncMock()
14
+ self.camera_service._run_action = AsyncMock()
15
+ self.camera_service._run_action_devicemgmt = AsyncMock()
16
+ self.camera_service._set_property = AsyncMock()
17
+ self.camera_service._set_property_list = AsyncMock()
18
+ self.camera_service._set_toggle = AsyncMock()
19
+ self.camera_service.get_updated_params = AsyncMock()
20
+
21
+ # Create a test camera
22
+ self.test_camera = Camera({
23
+ "device_type": DeviceTypes.CAMERA.value,
24
+ "product_model": "WYZEC1",
25
+ "mac": "TEST123",
26
+ "nickname": "Test Camera",
27
+ "device_params": {"ip": "192.168.1.100"},
28
+ "raw_dict": {}
29
+ })
30
+
31
+ async def test_update_legacy_camera(self):
32
+ # Mock responses
33
+ self.camera_service._get_event_list.return_value = {
34
+ 'data': {
35
+ 'event_list': [{
36
+ 'event_ts': 1234567890,
37
+ 'device_mac': 'TEST123',
38
+ 'event_type': 'motion'
39
+ }]
40
+ }
41
+ }
42
+
43
+ self.camera_service._get_property_list.return_value = [
44
+ (PropertyIDs.AVAILABLE, "1"),
45
+ (PropertyIDs.ON, "1"),
46
+ (PropertyIDs.CAMERA_SIREN, "0"),
47
+ (PropertyIDs.ACCESSORY, "0"),
48
+ (PropertyIDs.NOTIFICATION, "1"),
49
+ (PropertyIDs.MOTION_DETECTION, "1")
50
+ ]
51
+
52
+ updated_camera = await self.camera_service.update(self.test_camera)
53
+
54
+ self.assertTrue(updated_camera.available)
55
+ self.assertTrue(updated_camera.on)
56
+ self.assertFalse(updated_camera.siren)
57
+ self.assertFalse(updated_camera.floodlight)
58
+ self.assertTrue(updated_camera.notify)
59
+ self.assertTrue(updated_camera.motion)
60
+ self.assertIsNotNone(updated_camera.last_event)
61
+ self.assertEqual(updated_camera.last_event_ts, 1234567890)
62
+
63
+ async def test_update_devicemgmt_camera(self):
64
+ # Create a test camera using new API model
65
+ devicemgmt_camera = Camera({
66
+ "device_type": DeviceTypes.CAMERA.value,
67
+ "product_model": "LD_CFP", # Floodlight pro model
68
+ "mac": "TEST456",
69
+ "nickname": "Test DeviceMgmt Camera",
70
+ "device_params": {"ip": "192.168.1.101"},
71
+ "raw_dict": {}
72
+ })
73
+
74
+ self.camera_service._get_iot_prop_devicemgmt = AsyncMock(return_value={
75
+ 'data': {
76
+ 'capabilities': [
77
+ {
78
+ 'name': 'camera',
79
+ 'properties': {'motion-detect-recording': True}
80
+ },
81
+ {
82
+ 'name': 'floodlight',
83
+ 'properties': {'on': True}
84
+ },
85
+ {
86
+ 'name': 'siren',
87
+ 'properties': {'state': True}
88
+ },
89
+ {
90
+ 'name': 'iot-device',
91
+ 'properties': {
92
+ 'push-switch': True,
93
+ 'iot-power': True,
94
+ 'iot-state': True
95
+ }
96
+ }
97
+ ]
98
+ }
99
+ })
100
+
101
+ updated_camera = await self.camera_service.update(devicemgmt_camera)
102
+
103
+ self.assertTrue(updated_camera.available)
104
+ self.assertTrue(updated_camera.on)
105
+ self.assertTrue(updated_camera.siren)
106
+ self.assertTrue(updated_camera.floodlight)
107
+ self.assertTrue(updated_camera.notify)
108
+ self.assertTrue(updated_camera.motion)
109
+
110
+ async def test_turn_on_off_legacy_camera(self):
111
+ await self.camera_service.turn_on(self.test_camera)
112
+ self.camera_service._run_action.assert_awaited_with(self.test_camera, "power_on")
113
+
114
+ await self.camera_service.turn_off(self.test_camera)
115
+ self.camera_service._run_action.assert_awaited_with(self.test_camera, "power_off")
116
+
117
+ async def test_siren_control_legacy_camera(self):
118
+ await self.camera_service.siren_on(self.test_camera)
119
+ self.camera_service._run_action.assert_awaited_with(self.test_camera, "siren_on")
120
+
121
+ await self.camera_service.siren_off(self.test_camera)
122
+ self.camera_service._run_action.assert_awaited_with(self.test_camera, "siren_off")
123
+
124
+ async def test_floodlight_control_legacy_camera(self):
125
+ await self.camera_service.floodlight_on(self.test_camera)
126
+ self.camera_service._set_property.assert_awaited_with(
127
+ self.test_camera,
128
+ PropertyIDs.ACCESSORY.value,
129
+ "1"
130
+ )
131
+
132
+ await self.camera_service.floodlight_off(self.test_camera)
133
+ self.camera_service._set_property.assert_awaited_with(
134
+ self.test_camera,
135
+ PropertyIDs.ACCESSORY.value,
136
+ "2"
137
+ )
138
+
139
+ async def test_notification_control_legacy_camera(self):
140
+ await self.camera_service.turn_on_notifications(self.test_camera)
141
+ self.camera_service._set_property.assert_awaited_with(
142
+ self.test_camera,
143
+ PropertyIDs.NOTIFICATION.value,
144
+ "1"
145
+ )
146
+
147
+ await self.camera_service.turn_off_notifications(self.test_camera)
148
+ self.camera_service._set_property.assert_awaited_with(
149
+ self.test_camera,
150
+ PropertyIDs.NOTIFICATION.value,
151
+ "0"
152
+ )
153
+
154
+ async def test_motion_detection_control_legacy_camera(self):
155
+ await self.camera_service.turn_on_motion_detection(self.test_camera)
156
+ self.camera_service._set_property.assert_any_await(
157
+ self.test_camera,
158
+ PropertyIDs.MOTION_DETECTION.value,
159
+ "1"
160
+ )
161
+ self.camera_service._set_property.assert_any_await(
162
+ self.test_camera,
163
+ PropertyIDs.MOTION_DETECTION_TOGGLE.value,
164
+ "1"
165
+ )
166
+
167
+ await self.camera_service.turn_off_motion_detection(self.test_camera)
168
+ self.camera_service._set_property.assert_any_await(
169
+ self.test_camera,
170
+ PropertyIDs.MOTION_DETECTION.value,
171
+ "0"
172
+ )
173
+ self.camera_service._set_property.assert_any_await(
174
+ self.test_camera,
175
+ PropertyIDs.MOTION_DETECTION_TOGGLE.value,
176
+ "0"
177
+ )
178
+
179
+ if __name__ == '__main__':
180
+ unittest.main()
@@ -0,0 +1,90 @@
1
+ import unittest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from wyzeapy.services.hms_service import HMSService, HMSMode
4
+ from wyzeapy.wyze_auth_lib import WyzeAuthLib
5
+
6
+ class TestHMSService(unittest.IsolatedAsyncioTestCase):
7
+ async def asyncSetUp(self):
8
+ self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
9
+ self.hms_service = await HMSService.create(self.mock_auth_lib)
10
+ self.hms_service._get_plan_binding_list_by_user = AsyncMock()
11
+ self.hms_service._monitoring_profile_state_status = AsyncMock()
12
+ self.hms_service._monitoring_profile_active = AsyncMock()
13
+ self.hms_service._disable_reme_alarm = AsyncMock()
14
+
15
+ async def test_update_changing_mode(self):
16
+ self.hms_service._monitoring_profile_state_status.return_value = {'message': 'changing'}
17
+
18
+ mode = await self.hms_service.update('test_hms_id')
19
+ self.assertEqual(mode, HMSMode.CHANGING)
20
+
21
+ async def test_update_disarmed_mode(self):
22
+ self.hms_service._monitoring_profile_state_status.return_value = {'message': 'disarm'}
23
+
24
+ mode = await self.hms_service.update('test_hms_id')
25
+ self.assertEqual(mode, HMSMode.DISARMED)
26
+
27
+ async def test_update_away_mode(self):
28
+ self.hms_service._monitoring_profile_state_status.return_value = {'message': 'away'}
29
+
30
+ mode = await self.hms_service.update('test_hms_id')
31
+ self.assertEqual(mode, HMSMode.AWAY)
32
+
33
+ async def test_update_home_mode(self):
34
+ self.hms_service._monitoring_profile_state_status.return_value = {'message': 'home'}
35
+
36
+ mode = await self.hms_service.update('test_hms_id')
37
+ self.assertEqual(mode, HMSMode.HOME)
38
+
39
+ async def test_set_mode_disarmed(self):
40
+ self.hms_service._hms_id = 'test_hms_id'
41
+
42
+ await self.hms_service.set_mode(HMSMode.DISARMED)
43
+
44
+ self.hms_service._disable_reme_alarm.assert_awaited_with('test_hms_id')
45
+ self.hms_service._monitoring_profile_active.assert_awaited_with('test_hms_id', 0, 0)
46
+
47
+ async def test_set_mode_away(self):
48
+ self.hms_service._hms_id = 'test_hms_id'
49
+
50
+ await self.hms_service.set_mode(HMSMode.AWAY)
51
+
52
+ self.hms_service._monitoring_profile_active.assert_awaited_with('test_hms_id', 0, 1)
53
+
54
+ async def test_set_mode_home(self):
55
+ self.hms_service._hms_id = 'test_hms_id'
56
+
57
+ await self.hms_service.set_mode(HMSMode.HOME)
58
+
59
+ self.hms_service._monitoring_profile_active.assert_awaited_with('test_hms_id', 1, 0)
60
+
61
+ async def test_get_hms_id_with_existing_id(self):
62
+ self.hms_service._hms_id = 'existing_hms_id'
63
+ hms_id = await self.hms_service._get_hms_id()
64
+ self.assertEqual(hms_id, 'existing_hms_id')
65
+
66
+ async def test_get_hms_id_with_no_hms(self):
67
+ self.hms_service._hms_id = None
68
+ self.hms_service._get_plan_binding_list_by_user.return_value = {'data': []}
69
+
70
+ hms_id = await self.hms_service._get_hms_id()
71
+ self.assertIsNone(hms_id)
72
+
73
+ async def test_get_hms_id_finds_id(self):
74
+ self.hms_service._hms_id = None
75
+ self.hms_service._get_plan_binding_list_by_user.return_value = {
76
+ 'data': [
77
+ {
78
+ 'deviceList': [
79
+ {'device_id': 'found_hms_id'}
80
+ ]
81
+ }
82
+ ]
83
+ }
84
+
85
+ hms_id = await self.hms_service._get_hms_id()
86
+ self.assertEqual(hms_id, 'found_hms_id')
87
+ self.assertEqual(self.hms_service._hms_id, 'found_hms_id')
88
+
89
+ if __name__ == '__main__':
90
+ unittest.main()
@@ -0,0 +1,114 @@
1
+ import unittest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from wyzeapy.services.lock_service import LockService, Lock
4
+ from wyzeapy.types import DeviceTypes
5
+ from wyzeapy.exceptions import UnknownApiError
6
+
7
+ class TestLockService(unittest.IsolatedAsyncioTestCase):
8
+ async def asyncSetUp(self):
9
+ mock_auth_lib = MagicMock()
10
+ self.lock_service = LockService(auth_lib=mock_auth_lib)
11
+ self.lock_service._get_lock_info = AsyncMock()
12
+ self.lock_service._lock_control = AsyncMock()
13
+
14
+ async def test_update_lock_online(self):
15
+ mock_lock = Lock({
16
+ "device_type": "Lock",
17
+ "onoff_line": 1,
18
+ "door_open_status": 0,
19
+ "trash_mode": 0,
20
+ "locker_status": {"hardlock": 2},
21
+ "raw_dict": {}
22
+ })
23
+ self.lock_service._get_lock_info.return_value = {
24
+ "device": {
25
+ "onoff_line": 1,
26
+ "door_open_status": 0,
27
+ "trash_mode": 0,
28
+ "locker_status": {"hardlock": 2},
29
+ }
30
+ }
31
+
32
+ updated_lock = await self.lock_service.update(mock_lock)
33
+
34
+ self.assertTrue(updated_lock.available)
35
+ self.assertFalse(updated_lock.door_open)
36
+ self.assertFalse(updated_lock.trash_mode)
37
+ self.assertTrue(updated_lock.unlocked)
38
+ self.assertFalse(updated_lock.unlocking)
39
+ self.assertFalse(updated_lock.locking)
40
+ self.lock_service._get_lock_info.assert_awaited_once_with(mock_lock)
41
+
42
+ async def test_update_lock_offline(self):
43
+ mock_lock = Lock({
44
+ "device_type": "Lock",
45
+ "onoff_line": 0,
46
+ "door_open_status": 1,
47
+ "trash_mode": 1,
48
+ "locker_status": {"hardlock": 1},
49
+ "raw_dict": {}
50
+ })
51
+ self.lock_service._get_lock_info.return_value = {
52
+ "device": {
53
+ "onoff_line": 0,
54
+ "door_open_status": 1,
55
+ "trash_mode": 1,
56
+ "locker_status": {"hardlock": 1},
57
+ }
58
+ }
59
+
60
+ updated_lock = await self.lock_service.update(mock_lock)
61
+
62
+ self.assertFalse(updated_lock.available)
63
+ self.assertTrue(updated_lock.door_open)
64
+ self.assertTrue(updated_lock.trash_mode)
65
+ self.assertFalse(updated_lock.unlocked)
66
+ self.assertFalse(updated_lock.unlocking)
67
+ self.assertFalse(updated_lock.locking)
68
+ self.lock_service._get_lock_info.assert_awaited_once_with(mock_lock)
69
+
70
+ async def test_get_locks(self):
71
+ mock_device = AsyncMock()
72
+ mock_device.type = DeviceTypes.LOCK
73
+ mock_device.raw_dict = {"device_type": "Lock"}
74
+
75
+ self.lock_service.get_object_list = AsyncMock(return_value=[mock_device])
76
+
77
+ locks = await self.lock_service.get_locks()
78
+
79
+ self.assertEqual(len(locks), 1)
80
+ self.assertIsInstance(locks[0], Lock)
81
+ self.lock_service.get_object_list.assert_awaited_once()
82
+
83
+ async def test_lock(self):
84
+ mock_lock = Lock({
85
+ "device_type": "Lock",
86
+ "raw_dict": {}
87
+ })
88
+
89
+ await self.lock_service.lock(mock_lock)
90
+ self.lock_service._lock_control.assert_awaited_with(mock_lock, "remoteLock")
91
+
92
+ async def test_unlock(self):
93
+ mock_lock = Lock({
94
+ "device_type": "Lock",
95
+ "raw_dict": {}
96
+ })
97
+
98
+ await self.lock_service.unlock(mock_lock)
99
+ self.lock_service._lock_control.assert_awaited_with(mock_lock, "remoteUnlock")
100
+
101
+ async def test_lock_control_error_handling(self):
102
+ mock_lock = Lock({
103
+ "device_type": "Lock",
104
+ "raw_dict": {}
105
+ })
106
+ self.lock_service._lock_control.side_effect = UnknownApiError("Failed to lock/unlock")
107
+
108
+ with self.assertRaises(UnknownApiError):
109
+ await self.lock_service.lock(mock_lock)
110
+
111
+ with self.assertRaises(UnknownApiError):
112
+ await self.lock_service.unlock(mock_lock)
113
+
114
+ # ... other test cases ...
@@ -0,0 +1,159 @@
1
+ import unittest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from wyzeapy.services.sensor_service import SensorService, Sensor
4
+ from wyzeapy.types import DeviceTypes, PropertyIDs
5
+ from wyzeapy.wyze_auth_lib import WyzeAuthLib
6
+
7
+
8
+ class TestSensorService(unittest.IsolatedAsyncioTestCase):
9
+ async def asyncSetUp(self):
10
+ self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
11
+ self.sensor_service = SensorService(auth_lib=self.mock_auth_lib)
12
+ self.sensor_service._get_device_info = AsyncMock()
13
+ self.sensor_service.get_updated_params = AsyncMock()
14
+ self.sensor_service.get_object_list = AsyncMock()
15
+
16
+ # Reset the class-level subscribers list
17
+ self.sensor_service._subscribers = []
18
+
19
+ # Create test sensors
20
+ self.motion_sensor = Sensor({
21
+ "device_type": DeviceTypes.MOTION_SENSOR.value,
22
+ "product_model": "PIR3U",
23
+ "mac": "MOTION123",
24
+ "nickname": "Test Motion Sensor",
25
+ "device_params": {"ip": "192.168.1.100"},
26
+ "raw_dict": {}
27
+ })
28
+
29
+ self.contact_sensor = Sensor({
30
+ "device_type": DeviceTypes.CONTACT_SENSOR.value,
31
+ "product_model": "DWS3U",
32
+ "mac": "CONTACT456",
33
+ "nickname": "Test Contact Sensor",
34
+ "device_params": {"ip": "192.168.1.101"},
35
+ "raw_dict": {}
36
+ })
37
+
38
+ async def test_update_motion_sensor_detected(self):
39
+ self.sensor_service._get_device_info.return_value = {
40
+ 'data': {
41
+ 'property_list': [
42
+ {
43
+ 'pid': PropertyIDs.MOTION_STATE.value,
44
+ 'value': '1'
45
+ }
46
+ ]
47
+ }
48
+ }
49
+
50
+ updated_sensor = await self.sensor_service.update(self.motion_sensor)
51
+ self.assertTrue(updated_sensor.detected)
52
+
53
+ async def test_update_motion_sensor_not_detected(self):
54
+ self.sensor_service._get_device_info.return_value = {
55
+ 'data': {
56
+ 'property_list': [
57
+ {
58
+ 'pid': PropertyIDs.MOTION_STATE.value,
59
+ 'value': '0'
60
+ }
61
+ ]
62
+ }
63
+ }
64
+
65
+ updated_sensor = await self.sensor_service.update(self.motion_sensor)
66
+ self.assertFalse(updated_sensor.detected)
67
+
68
+ async def test_update_contact_sensor_detected(self):
69
+ self.sensor_service._get_device_info.return_value = {
70
+ 'data': {
71
+ 'property_list': [
72
+ {
73
+ 'pid': PropertyIDs.CONTACT_STATE.value,
74
+ 'value': '1'
75
+ }
76
+ ]
77
+ }
78
+ }
79
+
80
+ updated_sensor = await self.sensor_service.update(self.contact_sensor)
81
+ self.assertTrue(updated_sensor.detected)
82
+
83
+ async def test_update_contact_sensor_not_detected(self):
84
+ self.sensor_service._get_device_info.return_value = {
85
+ 'data': {
86
+ 'property_list': [
87
+ {
88
+ 'pid': PropertyIDs.CONTACT_STATE.value,
89
+ 'value': '0'
90
+ }
91
+ ]
92
+ }
93
+ }
94
+
95
+ updated_sensor = await self.sensor_service.update(self.contact_sensor)
96
+ self.assertFalse(updated_sensor.detected)
97
+
98
+ async def test_get_sensors(self):
99
+ mock_motion_device = MagicMock()
100
+ mock_motion_device.type = DeviceTypes.MOTION_SENSOR
101
+ mock_motion_device.raw_dict = {
102
+ "device_type": DeviceTypes.MOTION_SENSOR.value,
103
+ "product_model": "PIR3U",
104
+ "mac": "MOTION123"
105
+ }
106
+
107
+ mock_contact_device = MagicMock()
108
+ mock_contact_device.type = DeviceTypes.CONTACT_SENSOR
109
+ mock_contact_device.raw_dict = {
110
+ "device_type": DeviceTypes.CONTACT_SENSOR.value,
111
+ "product_model": "DWS3U",
112
+ "mac": "CONTACT456"
113
+ }
114
+
115
+ self.sensor_service.get_object_list.return_value = [
116
+ mock_motion_device,
117
+ mock_contact_device
118
+ ]
119
+
120
+ sensors = await self.sensor_service.get_sensors()
121
+
122
+ self.assertEqual(len(sensors), 2)
123
+ self.assertIsInstance(sensors[0], Sensor)
124
+ self.assertIsInstance(sensors[1], Sensor)
125
+ self.sensor_service.get_object_list.assert_awaited_once()
126
+
127
+ async def test_register_for_updates(self):
128
+ mock_callback = MagicMock()
129
+ await self.sensor_service.register_for_updates(self.motion_sensor, mock_callback)
130
+
131
+ self.assertEqual(len(self.sensor_service._subscribers), 1)
132
+ self.assertEqual(self.sensor_service._subscribers[0][0], self.motion_sensor)
133
+ self.assertEqual(self.sensor_service._subscribers[0][1], mock_callback)
134
+
135
+ async def test_deregister_for_updates(self):
136
+ mock_callback = MagicMock()
137
+ await self.sensor_service.register_for_updates(self.motion_sensor, mock_callback)
138
+ await self.sensor_service.deregister_for_updates(self.motion_sensor)
139
+
140
+ self.assertEqual(len(self.sensor_service._subscribers), 0)
141
+
142
+ async def test_update_with_unknown_property(self):
143
+ self.sensor_service._get_device_info.return_value = {
144
+ 'data': {
145
+ 'property_list': [
146
+ {
147
+ 'pid': 'unknown_property',
148
+ 'value': '1'
149
+ }
150
+ ]
151
+ }
152
+ }
153
+
154
+ updated_sensor = await self.sensor_service.update(self.motion_sensor)
155
+ self.assertFalse(updated_sensor.detected) # Should maintain default value
156
+
157
+
158
+ if __name__ == '__main__':
159
+ unittest.main()
@@ -0,0 +1,138 @@
1
+ import unittest
2
+ from datetime import datetime, timedelta
3
+ from unittest.mock import AsyncMock, MagicMock
4
+ from wyzeapy.services.switch_service import SwitchService, SwitchUsageService, Switch
5
+ from wyzeapy.types import DeviceTypes, PropertyIDs
6
+ from wyzeapy.wyze_auth_lib import WyzeAuthLib
7
+
8
+
9
+ class TestSwitchService(unittest.IsolatedAsyncioTestCase):
10
+ async def asyncSetUp(self):
11
+ self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
12
+ self.switch_service = SwitchService(auth_lib=self.mock_auth_lib)
13
+ self.switch_service._get_property_list = AsyncMock()
14
+ self.switch_service.get_updated_params = AsyncMock()
15
+ self.switch_service.get_object_list = AsyncMock()
16
+ self.switch_service._set_property = AsyncMock()
17
+
18
+ # Create test switch
19
+ self.test_switch = Switch({
20
+ "device_type": DeviceTypes.PLUG.value,
21
+ "product_model": "WLPP1",
22
+ "mac": "SWITCH123",
23
+ "nickname": "Test Switch",
24
+ "device_params": {"ip": "192.168.1.100"},
25
+ "raw_dict": {}
26
+ })
27
+
28
+ async def test_update_switch_on(self):
29
+ self.switch_service._get_property_list.return_value = [
30
+ (PropertyIDs.ON, "1"),
31
+ (PropertyIDs.AVAILABLE, "1")
32
+ ]
33
+
34
+ updated_switch = await self.switch_service.update(self.test_switch)
35
+
36
+ self.assertTrue(updated_switch.on)
37
+ self.assertTrue(updated_switch.available)
38
+
39
+ async def test_update_switch_off(self):
40
+ self.switch_service._get_property_list.return_value = [
41
+ (PropertyIDs.ON, "0"),
42
+ (PropertyIDs.AVAILABLE, "1")
43
+ ]
44
+
45
+ updated_switch = await self.switch_service.update(self.test_switch)
46
+
47
+ self.assertFalse(updated_switch.on)
48
+ self.assertTrue(updated_switch.available)
49
+
50
+ async def test_get_switches(self):
51
+ mock_plug = MagicMock()
52
+ mock_plug.type = DeviceTypes.PLUG
53
+ mock_plug.raw_dict = {
54
+ "device_type": DeviceTypes.PLUG.value,
55
+ "product_model": "WLPP1",
56
+ "mac": "PLUG123"
57
+ }
58
+
59
+ mock_outdoor_plug = MagicMock()
60
+ mock_outdoor_plug.type = DeviceTypes.OUTDOOR_PLUG
61
+ mock_outdoor_plug.raw_dict = {
62
+ "device_type": DeviceTypes.OUTDOOR_PLUG.value,
63
+ "product_model": "WLPPO",
64
+ "mac": "OUTPLUG456"
65
+ }
66
+
67
+ self.switch_service.get_object_list.return_value = [
68
+ mock_plug,
69
+ mock_outdoor_plug
70
+ ]
71
+
72
+ switches = await self.switch_service.get_switches()
73
+
74
+ self.assertEqual(len(switches), 2)
75
+ self.assertIsInstance(switches[0], Switch)
76
+ self.assertIsInstance(switches[1], Switch)
77
+ self.switch_service.get_object_list.assert_awaited_once()
78
+
79
+ async def test_turn_on(self):
80
+ await self.switch_service.turn_on(self.test_switch)
81
+ self.switch_service._set_property.assert_awaited_with(
82
+ self.test_switch,
83
+ PropertyIDs.ON.value,
84
+ "1"
85
+ )
86
+
87
+ async def test_turn_off(self):
88
+ await self.switch_service.turn_off(self.test_switch)
89
+ self.switch_service._set_property.assert_awaited_with(
90
+ self.test_switch,
91
+ PropertyIDs.ON.value,
92
+ "0"
93
+ )
94
+
95
+
96
+ class TestSwitchUsageService(unittest.IsolatedAsyncioTestCase):
97
+ async def asyncSetUp(self):
98
+ self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
99
+ self.usage_service = SwitchUsageService(auth_lib=self.mock_auth_lib)
100
+ self.usage_service._get_plug_history = AsyncMock()
101
+
102
+ # Create test switch
103
+ self.test_switch = Switch({
104
+ "device_type": DeviceTypes.PLUG.value,
105
+ "product_model": "WLPP1",
106
+ "mac": "SWITCH123",
107
+ "nickname": "Test Switch",
108
+ "device_params": {"ip": "192.168.1.100"},
109
+ "raw_dict": {}
110
+ })
111
+
112
+ async def test_update_usage_history(self):
113
+ mock_usage_data = {
114
+ "total_power": 100,
115
+ "time_series": [
116
+ {"power": 10, "timestamp": 1234567890},
117
+ {"power": 20, "timestamp": 1234567891}
118
+ ]
119
+ }
120
+ self.usage_service._get_plug_history.return_value = mock_usage_data
121
+
122
+ # Calculate expected timestamps
123
+ now = datetime.now()
124
+ expected_end_time = int(datetime.timestamp(now) * 1000)
125
+ expected_start_time = int(datetime.timestamp(now - timedelta(hours=25)) * 1000)
126
+
127
+ updated_switch = await self.usage_service.update(self.test_switch)
128
+
129
+ self.assertEqual(updated_switch.usage_history, mock_usage_data)
130
+ self.usage_service._get_plug_history.assert_awaited_with(
131
+ self.test_switch,
132
+ expected_start_time,
133
+ expected_end_time
134
+ )
135
+
136
+
137
+ if __name__ == '__main__':
138
+ unittest.main()
@@ -0,0 +1,136 @@
1
+ import unittest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from wyzeapy.services.thermostat_service import (
4
+ ThermostatService, Thermostat, HVACMode, FanMode,
5
+ TemperatureUnit, Preset, HVACState, ThermostatProps
6
+ )
7
+ from wyzeapy.types import DeviceTypes
8
+ from wyzeapy.wyze_auth_lib import WyzeAuthLib
9
+
10
+
11
+ class TestThermostatService(unittest.IsolatedAsyncioTestCase):
12
+ async def asyncSetUp(self):
13
+ self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
14
+ self.thermostat_service = ThermostatService(auth_lib=self.mock_auth_lib)
15
+ self.thermostat_service._thermostat_get_iot_prop = AsyncMock()
16
+ self.thermostat_service._thermostat_set_iot_prop = AsyncMock()
17
+ self.thermostat_service.get_object_list = AsyncMock()
18
+
19
+ # Create test thermostat
20
+ self.test_thermostat = Thermostat({
21
+ "device_type": DeviceTypes.THERMOSTAT.value,
22
+ "product_model": "WLPTH1",
23
+ "mac": "THERM123",
24
+ "nickname": "Test Thermostat",
25
+ "device_params": {"ip": "192.168.1.100"},
26
+ "raw_dict": {}
27
+ })
28
+
29
+ async def test_update_thermostat(self):
30
+ self.thermostat_service._thermostat_get_iot_prop.return_value = {
31
+ 'data': {
32
+ 'props': {
33
+ 'temp_unit': 'F',
34
+ 'cool_sp': '74',
35
+ 'heat_sp': '64',
36
+ 'fan_mode': 'auto',
37
+ 'mode_sys': 'auto',
38
+ 'current_scenario': 'home',
39
+ 'temperature': '71.5',
40
+ 'iot_state': 'connected',
41
+ 'humidity': '50',
42
+ 'working_state': 'idle'
43
+ }
44
+ }
45
+ }
46
+
47
+ updated_thermostat = await self.thermostat_service.update(self.test_thermostat)
48
+
49
+ self.assertEqual(updated_thermostat.temp_unit, TemperatureUnit.FAHRENHEIT)
50
+ self.assertEqual(updated_thermostat.cool_set_point, 74)
51
+ self.assertEqual(updated_thermostat.heat_set_point, 64)
52
+ self.assertEqual(updated_thermostat.fan_mode, FanMode.AUTO)
53
+ self.assertEqual(updated_thermostat.hvac_mode, HVACMode.AUTO)
54
+ self.assertEqual(updated_thermostat.preset, Preset.HOME)
55
+ self.assertEqual(updated_thermostat.temperature, 71.5)
56
+ self.assertTrue(updated_thermostat.available)
57
+ self.assertEqual(updated_thermostat.humidity, 50)
58
+ self.assertEqual(updated_thermostat.hvac_state, HVACState.IDLE)
59
+
60
+ async def test_get_thermostats(self):
61
+ mock_thermostat = MagicMock()
62
+ mock_thermostat.type = DeviceTypes.THERMOSTAT
63
+ mock_thermostat.raw_dict = {
64
+ "device_type": DeviceTypes.THERMOSTAT.value,
65
+ "product_model": "WLPTH1",
66
+ "mac": "THERM123"
67
+ }
68
+
69
+ self.thermostat_service.get_object_list.return_value = [mock_thermostat]
70
+
71
+ thermostats = await self.thermostat_service.get_thermostats()
72
+
73
+ self.assertEqual(len(thermostats), 1)
74
+ self.assertIsInstance(thermostats[0], Thermostat)
75
+ self.thermostat_service.get_object_list.assert_awaited_once()
76
+
77
+ async def test_set_cool_point(self):
78
+ await self.thermostat_service.set_cool_point(self.test_thermostat, 75)
79
+ self.thermostat_service._thermostat_set_iot_prop.assert_awaited_with(
80
+ self.test_thermostat,
81
+ ThermostatProps.COOL_SP,
82
+ 75
83
+ )
84
+
85
+ async def test_set_heat_point(self):
86
+ await self.thermostat_service.set_heat_point(self.test_thermostat, 68)
87
+ self.thermostat_service._thermostat_set_iot_prop.assert_awaited_with(
88
+ self.test_thermostat,
89
+ ThermostatProps.HEAT_SP,
90
+ 68
91
+ )
92
+
93
+ async def test_set_hvac_mode(self):
94
+ await self.thermostat_service.set_hvac_mode(self.test_thermostat, HVACMode.COOL)
95
+ self.thermostat_service._thermostat_set_iot_prop.assert_awaited_with(
96
+ self.test_thermostat,
97
+ ThermostatProps.MODE_SYS,
98
+ HVACMode.COOL.value
99
+ )
100
+
101
+ async def test_set_fan_mode(self):
102
+ await self.thermostat_service.set_fan_mode(self.test_thermostat, FanMode.ON)
103
+ self.thermostat_service._thermostat_set_iot_prop.assert_awaited_with(
104
+ self.test_thermostat,
105
+ ThermostatProps.FAN_MODE,
106
+ FanMode.ON.value
107
+ )
108
+
109
+ async def test_set_preset(self):
110
+ await self.thermostat_service.set_preset(self.test_thermostat, Preset.AWAY)
111
+ self.thermostat_service._thermostat_set_iot_prop.assert_awaited_with(
112
+ self.test_thermostat,
113
+ ThermostatProps.CURRENT_SCENARIO,
114
+ Preset.AWAY.value
115
+ )
116
+
117
+ async def test_update_with_invalid_property(self):
118
+ self.thermostat_service._thermostat_get_iot_prop.return_value = {
119
+ 'data': {
120
+ 'props': {
121
+ 'invalid_property': 'some_value',
122
+ 'temperature': '71.5'
123
+ }
124
+ }
125
+ }
126
+
127
+ updated_thermostat = await self.thermostat_service.update(self.test_thermostat)
128
+ self.assertEqual(updated_thermostat.temperature, 71.5)
129
+ # Other properties should maintain their default values
130
+ self.assertEqual(updated_thermostat.temp_unit, TemperatureUnit.FAHRENHEIT)
131
+ self.assertEqual(updated_thermostat.cool_set_point, 74)
132
+ self.assertEqual(updated_thermostat.heat_set_point, 64)
133
+
134
+
135
+ if __name__ == '__main__':
136
+ unittest.main()
@@ -0,0 +1,161 @@
1
+ import unittest
2
+ from unittest.mock import AsyncMock, MagicMock
3
+ from wyzeapy.services.wall_switch_service import (
4
+ WallSwitchService, WallSwitch, SinglePressType, WallSwitchProps
5
+ )
6
+ from wyzeapy.types import DeviceTypes
7
+ from wyzeapy.wyze_auth_lib import WyzeAuthLib
8
+
9
+
10
+ class TestWallSwitchService(unittest.IsolatedAsyncioTestCase):
11
+ async def asyncSetUp(self):
12
+ self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
13
+ self.wall_switch_service = WallSwitchService(auth_lib=self.mock_auth_lib)
14
+ self.wall_switch_service._wall_switch_get_iot_prop = AsyncMock()
15
+ self.wall_switch_service._wall_switch_set_iot_prop = AsyncMock()
16
+ self.wall_switch_service.get_object_list = AsyncMock()
17
+
18
+ # Create test wall switch
19
+ self.test_switch = WallSwitch({
20
+ "device_type": DeviceTypes.COMMON.value,
21
+ "product_model": "LD_SS1",
22
+ "mac": "SWITCH123",
23
+ "nickname": "Test Wall Switch",
24
+ "device_params": {"ip": "192.168.1.100"},
25
+ "raw_dict": {}
26
+ })
27
+
28
+ async def test_update_wall_switch(self):
29
+ self.wall_switch_service._wall_switch_get_iot_prop.return_value = {
30
+ 'data': {
31
+ 'props': {
32
+ 'iot_state': 'connected',
33
+ 'switch-power': True,
34
+ 'switch-iot': False,
35
+ 'single_press_type': 1
36
+ }
37
+ }
38
+ }
39
+
40
+ updated_switch = await self.wall_switch_service.update(self.test_switch)
41
+
42
+ self.assertTrue(updated_switch.available)
43
+ self.assertTrue(updated_switch.switch_power)
44
+ self.assertFalse(updated_switch.switch_iot)
45
+ self.assertEqual(updated_switch.single_press_type, SinglePressType.CLASSIC)
46
+ # Test the property that depends on single_press_type
47
+ self.assertTrue(updated_switch.on) # Should be True because switch_power is True and type is CLASSIC
48
+
49
+ async def test_update_wall_switch_iot_mode(self):
50
+ self.wall_switch_service._wall_switch_get_iot_prop.return_value = {
51
+ 'data': {
52
+ 'props': {
53
+ 'iot_state': 'connected',
54
+ 'switch-power': False,
55
+ 'switch-iot': True,
56
+ 'single_press_type': 2
57
+ }
58
+ }
59
+ }
60
+
61
+ updated_switch = await self.wall_switch_service.update(self.test_switch)
62
+
63
+ self.assertTrue(updated_switch.available)
64
+ self.assertFalse(updated_switch.switch_power)
65
+ self.assertTrue(updated_switch.switch_iot)
66
+ self.assertEqual(updated_switch.single_press_type, SinglePressType.IOT)
67
+ # Test the property that depends on single_press_type
68
+ self.assertTrue(updated_switch.on) # Should be True because switch_iot is True and type is IOT
69
+
70
+ async def test_get_switches(self):
71
+ mock_switch = MagicMock()
72
+ mock_switch.type = DeviceTypes.COMMON
73
+ mock_switch.product_model = "LD_SS1"
74
+ mock_switch.raw_dict = {
75
+ "device_type": DeviceTypes.COMMON.value,
76
+ "product_model": "LD_SS1",
77
+ "mac": "SWITCH123"
78
+ }
79
+
80
+ # Add a non-wall switch device to test filtering
81
+ mock_other_device = MagicMock()
82
+ mock_other_device.type = DeviceTypes.COMMON
83
+ mock_other_device.product_model = "OTHER_MODEL"
84
+
85
+ self.wall_switch_service.get_object_list.return_value = [
86
+ mock_switch,
87
+ mock_other_device
88
+ ]
89
+
90
+ switches = await self.wall_switch_service.get_switches()
91
+
92
+ self.assertEqual(len(switches), 1)
93
+ self.assertIsInstance(switches[0], WallSwitch)
94
+ self.wall_switch_service.get_object_list.assert_awaited_once()
95
+
96
+ async def test_turn_on_classic_mode(self):
97
+ self.test_switch.single_press_type = SinglePressType.CLASSIC
98
+ await self.wall_switch_service.turn_on(self.test_switch)
99
+ self.wall_switch_service._wall_switch_set_iot_prop.assert_awaited_with(
100
+ self.test_switch,
101
+ WallSwitchProps.SWITCH_POWER,
102
+ True
103
+ )
104
+
105
+ async def test_turn_off_classic_mode(self):
106
+ self.test_switch.single_press_type = SinglePressType.CLASSIC
107
+ await self.wall_switch_service.turn_off(self.test_switch)
108
+ self.wall_switch_service._wall_switch_set_iot_prop.assert_awaited_with(
109
+ self.test_switch,
110
+ WallSwitchProps.SWITCH_POWER,
111
+ False
112
+ )
113
+
114
+ async def test_turn_on_iot_mode(self):
115
+ self.test_switch.single_press_type = SinglePressType.IOT
116
+ await self.wall_switch_service.turn_on(self.test_switch)
117
+ self.wall_switch_service._wall_switch_set_iot_prop.assert_awaited_with(
118
+ self.test_switch,
119
+ WallSwitchProps.SWITCH_IOT,
120
+ True
121
+ )
122
+
123
+ async def test_turn_off_iot_mode(self):
124
+ self.test_switch.single_press_type = SinglePressType.IOT
125
+ await self.wall_switch_service.turn_off(self.test_switch)
126
+ self.wall_switch_service._wall_switch_set_iot_prop.assert_awaited_with(
127
+ self.test_switch,
128
+ WallSwitchProps.SWITCH_IOT,
129
+ False
130
+ )
131
+
132
+ async def test_set_single_press_type(self):
133
+ await self.wall_switch_service.set_single_press_type(
134
+ self.test_switch,
135
+ SinglePressType.IOT
136
+ )
137
+ self.wall_switch_service._wall_switch_set_iot_prop.assert_awaited_with(
138
+ self.test_switch,
139
+ WallSwitchProps.SINGLE_PRESS_TYPE,
140
+ SinglePressType.IOT.value
141
+ )
142
+
143
+ async def test_update_with_invalid_property(self):
144
+ self.wall_switch_service._wall_switch_get_iot_prop.return_value = {
145
+ 'data': {
146
+ 'props': {
147
+ 'invalid_property': 'some_value',
148
+ 'switch-power': True
149
+ }
150
+ }
151
+ }
152
+
153
+ updated_switch = await self.wall_switch_service.update(self.test_switch)
154
+ self.assertTrue(updated_switch.switch_power)
155
+ # Other properties should maintain their default values
156
+ self.assertEqual(updated_switch.single_press_type, SinglePressType.CLASSIC)
157
+ self.assertFalse(updated_switch.switch_iot)
158
+
159
+
160
+ if __name__ == '__main__':
161
+ unittest.main()
wyzeapy/types.py CHANGED
@@ -28,6 +28,7 @@ class DeviceTypes(Enum):
28
28
  CHIME_SENSOR = "ChimeSensor"
29
29
  CONTACT_SENSOR = "ContactSensor"
30
30
  MOTION_SENSOR = "MotionSensor"
31
+ LEAK_SENSOR = "LeakSensor"
31
32
  WRIST = "Wrist"
32
33
  BASE_STATION = "BaseStation"
33
34
  SCALE = "WyzeScale"
@@ -43,6 +44,7 @@ class DeviceTypes(Enum):
43
44
  KEYPAD = "Keypad"
44
45
  LIGHTSTRIP = "LightStrip"
45
46
 
47
+
46
48
  class Device:
47
49
  product_type: str
48
50
  product_model: str
@@ -102,7 +104,7 @@ class PropertyIDs(Enum):
102
104
  CONTACT_STATE = "P1301"
103
105
  MOTION_STATE = "P1302"
104
106
  CAMERA_SIREN = "P1049"
105
- FLOOD_LIGHT = "P1056" # Also lamp socket on v3/v4 with lamp socket accessory
107
+ ACCESSORY = "P1056" # Is state for camera accessories, like garage doors, light sockets, and floodlights.
106
108
  SUN_MATCH = "P1528"
107
109
  MOTION_DETECTION = "P1047" # Current Motion Detection State of the Camera
108
110
  MOTION_DETECTION_TOGGLE = "P1001" # This toggles Camera Motion Detection On/Off
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wyzeapy
3
- Version: 0.5.26
3
+ Version: 0.5.27
4
4
  Summary: A library for interacting with Wyze devices
5
5
  License: GPL-3.0-only
6
6
  Author: Katie Mulliken
@@ -10,7 +10,6 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
- Classifier: Programming Language :: Python :: 3.13
14
13
  Requires-Dist: aiodns (>=3.2.0,<4.0.0)
15
- Requires-Dist: aiohttp (>=3.10.8,<4.0.0)
14
+ Requires-Dist: aiohttp (>=3.11.12,<4.0.0)
16
15
  Requires-Dist: pycryptodome (>=3.21.0,<4.0.0)
@@ -4,9 +4,9 @@ wyzeapy/crypto.py,sha256=EI9WkKq4jS0nZS5CIEOSYD_9WtM7kEzrhqEg6YdPUIo,1342
4
4
  wyzeapy/exceptions.py,sha256=BVRF7GMMC2T_ZSfGoIVHQsEjVQ3URd5xT6OYqfNxkY0,759
5
5
  wyzeapy/payload_factory.py,sha256=PtnxN9awH4Wh7FPXrGhfHvyGPPYLLZc4XQ2ldZIZZjk,18897
6
6
  wyzeapy/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- wyzeapy/services/base_service.py,sha256=vGbEirVXyDrIMJ1aHd2ZYbgn8oA77Rryq2uU24NlIiY,28142
7
+ wyzeapy/services/base_service.py,sha256=wEn_QFiTJ6-L6bpx3OdBpTU3XnNo-BjKHH4VJ7Bdjzg,28150
8
8
  wyzeapy/services/bulb_service.py,sha256=n2Pvuunz5d7Iv275vDQjKxByowBDkVoVrztBLyDAOZE,6840
9
- wyzeapy/services/camera_service.py,sha256=mco8YvhybiNUifBaMIf8oTGKc12D73EEAPydtB8ZhQA,9379
9
+ wyzeapy/services/camera_service.py,sha256=KBVDwmmSOHCzlXao9PGFKwvhSeGXttMOgOkI-3DpluU,9931
10
10
  wyzeapy/services/hms_service.py,sha256=3Fhqlmk96dm8LFajjW6P40DCIYMudfN3YIDoVIonqGw,2459
11
11
  wyzeapy/services/lock_service.py,sha256=cQ3A8nuovqPoofVBoo4dnnP-3GAkrf5Kgn0dPJWyhc0,1780
12
12
  wyzeapy/services/sensor_service.py,sha256=xI--Zr4Dm6DuZrV5pNxBVA1C6rrfhG4T0AFkvXCxVP8,3252
@@ -14,10 +14,18 @@ wyzeapy/services/switch_service.py,sha256=8LGZ1MloLGEXLVy8PaUwfoEzCXdwefLIyGUc2a
14
14
  wyzeapy/services/thermostat_service.py,sha256=3yJ8Jx7WPROTkiD2868QrfALFR1QB6_JI4LqcMzOOVc,5228
15
15
  wyzeapy/services/update_manager.py,sha256=036ClmJMFzXjD03mfg5DIyjB3xwqHoWkmpU2tcquc5Q,6132
16
16
  wyzeapy/services/wall_switch_service.py,sha256=gc6RDDJ5mceiKQUUs0n1wUKM2Y9Hj4_V2SLX9PxRa0g,4262
17
- wyzeapy/types.py,sha256=22v06L9tugwo2BS-lzM7fRbQHkk4a_FQhzTNlpA7tPA,6366
17
+ wyzeapy/tests/test_bulb_service.py,sha256=Bwi34HTGGi3S9JYsFwXgI8TXZx604FSMKNNOaOLuWLs,4910
18
+ wyzeapy/tests/test_camera_service.py,sha256=8xIrHmY25UDnj3angSgDaYvlq7b1bVgMu6VU9ch1EEs,6883
19
+ wyzeapy/tests/test_hms_service.py,sha256=STyBUtet1ytHxLcqZN9tpTOh84CdQpU7iKjDWNjijRA,3503
20
+ wyzeapy/tests/test_lock_service.py,sha256=QYMaaocVcw9ylqjnS8oP6sZH6oKfmB-PhaZZhLPNt4Q,3947
21
+ wyzeapy/tests/test_sensor_service.py,sha256=gm-qerFmiThAW_nb2L46IVuKWNeX_rTMyQPBg8TNbRQ,5676
22
+ wyzeapy/tests/test_switch_service.py,sha256=SLoLOiZk0kIqkp36_oaOK8nUvSy585Rc8mm9pNuDQqI,4812
23
+ wyzeapy/tests/test_thermostat_service.py,sha256=XL893tBuEexbcykhfS6ZxFMPuNXuQCpR-Kj2qKnVroo,5443
24
+ wyzeapy/tests/test_wall_switch_service.py,sha256=NHbCvVrwutS3aahlOyaTWrTerEOdHbO1fgdv86OiPQA,6238
25
+ wyzeapy/types.py,sha256=gG6V4YOklcJGnkPKp7exPQeCvHOl7Vcz7-iHZ8duKhc,6427
18
26
  wyzeapy/utils.py,sha256=vxwshLArRvP7g3qpFBdFcXBCM2741ItNSSsfdeHD5GU,4755
19
27
  wyzeapy/wyze_auth_lib.py,sha256=ERDuTQiTpbcNH5-Z38dGFTbLynvkejghp_eCYtb7OQQ,13712
20
- wyzeapy-0.5.26.dist-info/LICENSES/GPL-3.0-only.txt,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
21
- wyzeapy-0.5.26.dist-info/METADATA,sha256=70rhe4o6i3JtjvfauVN_vsD4EA0pyWEWaXVieLIOapg,613
22
- wyzeapy-0.5.26.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
23
- wyzeapy-0.5.26.dist-info/RECORD,,
28
+ wyzeapy-0.5.27.dist-info/LICENSES/GPL-3.0-only.txt,sha256=tqi_Y64slbCqJW7ndGgNe9GPIfRX2nVGb3YQs7FqzE4,34670
29
+ wyzeapy-0.5.27.dist-info/METADATA,sha256=58N37dVxn_gBkvOM09iDpKdpRU8wD7xvTRoUmbJxi9g,563
30
+ wyzeapy-0.5.27.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
31
+ wyzeapy-0.5.27.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any