wyzeapy 0.5.26__tar.gz → 0.5.27__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.
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/PKG-INFO +2 -3
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/pyproject.toml +3 -3
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/base_service.py +1 -3
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/camera_service.py +14 -4
- wyzeapy-0.5.27/src/wyzeapy/tests/test_bulb_service.py +135 -0
- wyzeapy-0.5.27/src/wyzeapy/tests/test_camera_service.py +180 -0
- wyzeapy-0.5.27/src/wyzeapy/tests/test_hms_service.py +90 -0
- wyzeapy-0.5.27/src/wyzeapy/tests/test_lock_service.py +114 -0
- wyzeapy-0.5.27/src/wyzeapy/tests/test_sensor_service.py +159 -0
- wyzeapy-0.5.27/src/wyzeapy/tests/test_switch_service.py +138 -0
- wyzeapy-0.5.27/src/wyzeapy/tests/test_thermostat_service.py +136 -0
- wyzeapy-0.5.27/src/wyzeapy/tests/test_wall_switch_service.py +161 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/types.py +3 -1
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/LICENSES/GPL-3.0-only.txt +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/__init__.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/const.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/crypto.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/exceptions.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/payload_factory.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/__init__.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/bulb_service.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/hms_service.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/lock_service.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/sensor_service.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/switch_service.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/thermostat_service.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/update_manager.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/services/wall_switch_service.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/utils.py +0 -0
- {wyzeapy-0.5.26 → wyzeapy-0.5.27}/src/wyzeapy/wyze_auth_lib.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: wyzeapy
|
|
3
|
-
Version: 0.5.
|
|
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.
|
|
14
|
+
Requires-Dist: aiohttp (>=3.11.12,<4.0.0)
|
|
16
15
|
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.
|
|
3
|
+
version = "0.5.27"
|
|
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
|
+
aiohttp = "^3.11.12"
|
|
11
11
|
aiodns = "^3.2.0"
|
|
12
12
|
pycryptodome = "^3.21.0"
|
|
13
13
|
|
|
14
|
-
[tool.poetry.dev
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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()
|
|
@@ -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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|