wyzeapy 0.5.26__tar.gz → 0.5.28__tar.gz

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.
Files changed (30) hide show
  1. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/PKG-INFO +3 -3
  2. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/pyproject.toml +3 -3
  3. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/base_service.py +20 -3
  4. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/camera_service.py +14 -4
  5. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/lock_service.py +9 -0
  6. wyzeapy-0.5.28/src/wyzeapy/tests/test_bulb_service.py +135 -0
  7. wyzeapy-0.5.28/src/wyzeapy/tests/test_camera_service.py +180 -0
  8. wyzeapy-0.5.28/src/wyzeapy/tests/test_hms_service.py +90 -0
  9. wyzeapy-0.5.28/src/wyzeapy/tests/test_lock_service.py +114 -0
  10. wyzeapy-0.5.28/src/wyzeapy/tests/test_sensor_service.py +159 -0
  11. wyzeapy-0.5.28/src/wyzeapy/tests/test_switch_service.py +138 -0
  12. wyzeapy-0.5.28/src/wyzeapy/tests/test_thermostat_service.py +136 -0
  13. wyzeapy-0.5.28/src/wyzeapy/tests/test_wall_switch_service.py +161 -0
  14. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/types.py +3 -1
  15. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/utils.py +15 -0
  16. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/LICENSES/GPL-3.0-only.txt +0 -0
  17. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/__init__.py +0 -0
  18. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/const.py +0 -0
  19. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/crypto.py +0 -0
  20. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/exceptions.py +0 -0
  21. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/payload_factory.py +0 -0
  22. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/__init__.py +0 -0
  23. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/bulb_service.py +0 -0
  24. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/hms_service.py +0 -0
  25. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/sensor_service.py +0 -0
  26. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/switch_service.py +0 -0
  27. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/thermostat_service.py +0 -0
  28. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/update_manager.py +0 -0
  29. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/services/wall_switch_service.py +0 -0
  30. {wyzeapy-0.5.26 → wyzeapy-0.5.28}/src/wyzeapy/wyze_auth_lib.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: wyzeapy
3
- Version: 0.5.26
3
+ Version: 0.5.28
4
4
  Summary: A library for interacting with Wyze devices
5
5
  License: GPL-3.0-only
6
6
  Author: Katie Mulliken
@@ -12,5 +12,5 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: aiodns (>=3.2.0,<4.0.0)
15
- Requires-Dist: aiohttp (>=3.10.8,<4.0.0)
15
+ Requires-Dist: aiohttp (>=3.11.12,<4.0.0)
16
16
  Requires-Dist: pycryptodome (>=3.21.0,<4.0.0)
@@ -1,17 +1,17 @@
1
1
  [tool.poetry]
2
2
  name = "wyzeapy"
3
- version = "0.5.26"
3
+ version = "0.5.28"
4
4
  description = "A library for interacting with Wyze devices"
5
5
  authors = ["Katie Mulliken <katie@mulliken.net>"]
6
6
  license = "GPL-3.0-only"
7
7
 
8
8
  [tool.poetry.dependencies]
9
9
  python = ">=3.11.0"
10
- aiohttp = "^3.10.8"
10
+ aiohttp = "^3.11.12"
11
11
  aiodns = "^3.2.0"
12
12
  pycryptodome = "^3.21.0"
13
13
 
14
- [tool.poetry.dev-dependencies]
14
+ [tool.poetry.group.dev.dependencies]
15
15
 
16
16
  [build-system]
17
17
  requires = ["poetry-core>=1.0.0"]
@@ -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:
@@ -628,6 +626,25 @@ class BaseService:
628
626
 
629
627
  return response_json
630
628
 
629
+ async def _get_lock_ble_token(self, device: Device) -> Dict[str, Optional[Any]]:
630
+ await self._auth_lib.refresh_if_should()
631
+
632
+ url_path = "/openapi/lock/v1/ble/token"
633
+
634
+ payload = {
635
+ "uuid": device.mac
636
+ }
637
+
638
+ payload = ford_create_payload(self._auth_lib.token.access_token, payload, url_path, "get")
639
+
640
+ url = f"https://yd-saas-toc.wyzecam.com{url_path}"
641
+
642
+ response_json = await self._auth_lib.get(url, params=payload)
643
+
644
+ check_for_errors_lock(self, response_json)
645
+
646
+ return response_json
647
+
631
648
  async def _get_device_info(self, device: Device) -> Dict[Any, Any]:
632
649
  await self._auth_lib.refresh_if_should()
633
650
 
@@ -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")
@@ -4,7 +4,9 @@
4
4
  # the license with this file. If not, please write to:
5
5
  # katie@mulliken.net to receive a copy
6
6
  from .base_service import BaseService
7
+ from ..const import FORD_APP_SECRET
7
8
  from ..types import Device, DeviceTypes
9
+ from ..utils import wyze_decrypt_cbc
8
10
 
9
11
 
10
12
  class Lock(Device):
@@ -13,12 +15,19 @@ class Lock(Device):
13
15
  unlocking = False
14
16
  door_open = False
15
17
  trash_mode = False
18
+ ble_id = None
19
+ ble_token = None
16
20
 
17
21
 
18
22
  class LockService(BaseService):
19
23
  async def update(self, lock: Lock):
20
24
  device_info = await self._get_lock_info(lock)
21
25
  lock.raw_dict = device_info["device"]
26
+ if lock.product_model == "YD_BT1":
27
+ ble_token_info = await self._get_lock_ble_token(lock)
28
+ lock.raw_dict["token"] = ble_token_info["token"]
29
+ lock.ble_id = ble_token_info["token"]["id"]
30
+ lock.ble_token = wyze_decrypt_cbc(FORD_APP_SECRET[:16], ble_token_info["token"]["token"])
22
31
 
23
32
  lock.available = lock.raw_dict.get("onoff_line") == 1
24
33
  lock.door_open = lock.raw_dict.get("door_open_status") == 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 ...