wyzeapy 0.5.28__py3-none-any.whl → 0.5.30__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.
- wyzeapy/__init__.py +277 -45
- wyzeapy/const.py +9 -4
- wyzeapy/crypto.py +31 -2
- wyzeapy/exceptions.py +11 -8
- wyzeapy/payload_factory.py +205 -170
- wyzeapy/services/__init__.py +3 -0
- wyzeapy/services/base_service.py +406 -212
- wyzeapy/services/bulb_service.py +67 -63
- wyzeapy/services/camera_service.py +136 -50
- wyzeapy/services/hms_service.py +8 -17
- wyzeapy/services/irrigation_service.py +189 -0
- wyzeapy/services/lock_service.py +5 -3
- wyzeapy/services/sensor_service.py +32 -11
- wyzeapy/services/switch_service.py +6 -2
- wyzeapy/services/thermostat_service.py +29 -15
- wyzeapy/services/update_manager.py +38 -11
- wyzeapy/services/wall_switch_service.py +18 -8
- wyzeapy/tests/test_irrigation_service.py +536 -0
- wyzeapy/types.py +29 -12
- wyzeapy/utils.py +98 -17
- wyzeapy/wyze_auth_lib.py +195 -37
- wyzeapy-0.5.30.dist-info/METADATA +13 -0
- wyzeapy-0.5.30.dist-info/RECORD +24 -0
- {wyzeapy-0.5.28.dist-info → wyzeapy-0.5.30.dist-info}/WHEEL +1 -1
- wyzeapy/tests/test_bulb_service.py +0 -135
- wyzeapy/tests/test_camera_service.py +0 -180
- wyzeapy/tests/test_hms_service.py +0 -90
- wyzeapy/tests/test_lock_service.py +0 -114
- wyzeapy/tests/test_sensor_service.py +0 -159
- wyzeapy/tests/test_switch_service.py +0 -138
- wyzeapy/tests/test_thermostat_service.py +0 -136
- wyzeapy/tests/test_wall_switch_service.py +0 -161
- wyzeapy-0.5.28.dist-info/LICENSES/GPL-3.0-only.txt +0 -232
- wyzeapy-0.5.28.dist-info/METADATA +0 -16
- wyzeapy-0.5.28.dist-info/RECORD +0 -31
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
3
|
+
from wyzeapy.services.irrigation_service import (
|
|
4
|
+
IrrigationService, Irrigation, Zone
|
|
5
|
+
)
|
|
6
|
+
from wyzeapy.types import DeviceTypes, Device
|
|
7
|
+
from wyzeapy.wyze_auth_lib import WyzeAuthLib
|
|
8
|
+
|
|
9
|
+
# todo: add tests for irrigation service
|
|
10
|
+
|
|
11
|
+
class TestIrrigationService(unittest.IsolatedAsyncioTestCase):
|
|
12
|
+
async def asyncSetUp(self):
|
|
13
|
+
self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
|
|
14
|
+
self.irrigation_service = IrrigationService(auth_lib=self.mock_auth_lib)
|
|
15
|
+
self.irrigation_service.get_iot_prop = AsyncMock()
|
|
16
|
+
self.irrigation_service.get_zone_by_device = AsyncMock()
|
|
17
|
+
self.irrigation_service.get_object_list = AsyncMock()
|
|
18
|
+
self.irrigation_service._get_iot_prop = AsyncMock()
|
|
19
|
+
self.irrigation_service._get_zone_by_device = AsyncMock()
|
|
20
|
+
self.irrigation_service._irrigation_device_info = AsyncMock()
|
|
21
|
+
|
|
22
|
+
# Create test irrigation
|
|
23
|
+
self.test_irrigation = Irrigation({
|
|
24
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
25
|
+
"product_model": "BS_WK1",
|
|
26
|
+
"mac": "IRRIG123",
|
|
27
|
+
"nickname": "Test Irrigation",
|
|
28
|
+
"device_params": {"ip": "192.168.1.100"},
|
|
29
|
+
"raw_dict": {}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
async def test_update_irrigation(self):
|
|
33
|
+
# Mock IoT properties response
|
|
34
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
35
|
+
'data': {
|
|
36
|
+
'props': {
|
|
37
|
+
'RSSI': '-65',
|
|
38
|
+
'IP': '192.168.1.100',
|
|
39
|
+
'sn': 'SN123456789',
|
|
40
|
+
'ssid': 'TestSSID',
|
|
41
|
+
'iot_state': 'connected'
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Mock zones response
|
|
47
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
48
|
+
'data': {
|
|
49
|
+
'zones': [
|
|
50
|
+
{
|
|
51
|
+
'zone_number': 1,
|
|
52
|
+
'name': 'Zone 1',
|
|
53
|
+
'enabled': True,
|
|
54
|
+
'zone_id': 'zone1',
|
|
55
|
+
'smart_duration': 600
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
'zone_number': 2,
|
|
59
|
+
'name': 'Zone 2',
|
|
60
|
+
'enabled': True,
|
|
61
|
+
'zone_id': 'zone2',
|
|
62
|
+
'smart_duration': 900
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
69
|
+
|
|
70
|
+
# Test IoT properties
|
|
71
|
+
self.assertEqual(updated_irrigation.RSSI, '-65')
|
|
72
|
+
self.assertEqual(updated_irrigation.IP, '192.168.1.100')
|
|
73
|
+
self.assertEqual(updated_irrigation.sn, 'SN123456789')
|
|
74
|
+
self.assertEqual(updated_irrigation.ssid, 'TestSSID')
|
|
75
|
+
self.assertTrue(updated_irrigation.available)
|
|
76
|
+
|
|
77
|
+
# Test zones
|
|
78
|
+
self.assertEqual(len(updated_irrigation.zones), 2)
|
|
79
|
+
self.assertEqual(updated_irrigation.zones[0].zone_number, 1)
|
|
80
|
+
self.assertEqual(updated_irrigation.zones[0].name, 'Zone 1')
|
|
81
|
+
self.assertTrue(updated_irrigation.zones[0].enabled)
|
|
82
|
+
self.assertEqual(updated_irrigation.zones[0].quickrun_duration, 600)
|
|
83
|
+
self.assertEqual(updated_irrigation.zones[1].zone_number, 2)
|
|
84
|
+
self.assertEqual(updated_irrigation.zones[1].name, 'Zone 2')
|
|
85
|
+
self.assertTrue(updated_irrigation.zones[1].enabled)
|
|
86
|
+
self.assertEqual(updated_irrigation.zones[1].quickrun_duration, 900)
|
|
87
|
+
|
|
88
|
+
async def test_get_irrigations(self):
|
|
89
|
+
# Create a mock irrigation device with all required attributes
|
|
90
|
+
mock_irrigation = MagicMock()
|
|
91
|
+
mock_irrigation.type = DeviceTypes.IRRIGATION
|
|
92
|
+
mock_irrigation.product_model = "BS_WK1"
|
|
93
|
+
mock_irrigation.raw_dict = {
|
|
94
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
95
|
+
"product_model": "BS_WK1",
|
|
96
|
+
"mac": "IRRIG123",
|
|
97
|
+
"nickname": "Test Irrigation",
|
|
98
|
+
"device_params": {"ip": "192.168.1.100"},
|
|
99
|
+
"raw_dict": {}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Mock the get_object_list to return our mock irrigation device
|
|
103
|
+
self.irrigation_service.get_object_list.return_value = [mock_irrigation]
|
|
104
|
+
|
|
105
|
+
# Get the irrigations
|
|
106
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
107
|
+
|
|
108
|
+
# Verify the results
|
|
109
|
+
self.assertEqual(len(irrigations), 1)
|
|
110
|
+
self.assertIsInstance(irrigations[0], Irrigation)
|
|
111
|
+
self.assertEqual(irrigations[0].product_model, "BS_WK1")
|
|
112
|
+
self.assertEqual(irrigations[0].mac, "IRRIG123")
|
|
113
|
+
self.irrigation_service.get_object_list.assert_awaited_once()
|
|
114
|
+
|
|
115
|
+
async def test_set_zone_quickrun_duration(self):
|
|
116
|
+
# Setup test irrigation with zones
|
|
117
|
+
self.test_irrigation.zones = [
|
|
118
|
+
Zone({
|
|
119
|
+
'zone_number': 1,
|
|
120
|
+
'name': 'Zone 1',
|
|
121
|
+
'enabled': True,
|
|
122
|
+
'zone_id': 'zone1',
|
|
123
|
+
'smart_duration': 400
|
|
124
|
+
}),
|
|
125
|
+
Zone({
|
|
126
|
+
'zone_number': 2,
|
|
127
|
+
'name': 'Zone 2',
|
|
128
|
+
'enabled': True,
|
|
129
|
+
'zone_id': 'zone2',
|
|
130
|
+
'smart_duration': 900
|
|
131
|
+
})
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
# Test setting quickrun duration
|
|
135
|
+
await self.irrigation_service.set_zone_quickrun_duration(
|
|
136
|
+
self.test_irrigation,
|
|
137
|
+
1,
|
|
138
|
+
300
|
|
139
|
+
)
|
|
140
|
+
self.assertEqual(self.test_irrigation.zones[0].quickrun_duration, 300)
|
|
141
|
+
|
|
142
|
+
# Test setting quickrun duration for non-existent zone
|
|
143
|
+
await self.irrigation_service.set_zone_quickrun_duration(
|
|
144
|
+
self.test_irrigation,
|
|
145
|
+
999,
|
|
146
|
+
300
|
|
147
|
+
)
|
|
148
|
+
# Verify that no zones were modified
|
|
149
|
+
self.assertEqual(len(self.test_irrigation.zones), 2)
|
|
150
|
+
self.assertEqual(self.test_irrigation.zones[0].quickrun_duration, 300) # First zone changed to 300
|
|
151
|
+
self.assertEqual(self.test_irrigation.zones[1].quickrun_duration, 900) # Second zone should be unchanged at 900
|
|
152
|
+
|
|
153
|
+
async def test_update_with_invalid_property(self):
|
|
154
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
155
|
+
'data': {
|
|
156
|
+
'props': {
|
|
157
|
+
'invalid_property': 'some_value',
|
|
158
|
+
'RSSI': '-65'
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
164
|
+
'data': {
|
|
165
|
+
'zones': []
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
170
|
+
self.assertEqual(updated_irrigation.RSSI, '-65')
|
|
171
|
+
# Other properties should maintain their default values
|
|
172
|
+
self.assertEqual(updated_irrigation.IP, "192.168.1.100")
|
|
173
|
+
self.assertEqual(updated_irrigation.sn, "SN123456789")
|
|
174
|
+
self.assertEqual(updated_irrigation.ssid, "ssid")
|
|
175
|
+
|
|
176
|
+
async def test_start_zone(self):
|
|
177
|
+
# Mock the _start_zone method
|
|
178
|
+
self.irrigation_service._start_zone = AsyncMock()
|
|
179
|
+
expected_response = {'data': {'result': 'success'}}
|
|
180
|
+
self.irrigation_service._start_zone.return_value = expected_response
|
|
181
|
+
|
|
182
|
+
# Test starting a zone
|
|
183
|
+
result = await self.irrigation_service.start_zone(
|
|
184
|
+
self.test_irrigation,
|
|
185
|
+
zone_number=1,
|
|
186
|
+
quickrun_duration=300
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Verify the call was made with correct parameters
|
|
190
|
+
self.irrigation_service._start_zone.assert_awaited_once_with(
|
|
191
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/quickrun",
|
|
192
|
+
self.test_irrigation,
|
|
193
|
+
1,
|
|
194
|
+
300
|
|
195
|
+
)
|
|
196
|
+
self.assertEqual(result, expected_response)
|
|
197
|
+
|
|
198
|
+
async def test_stop_running_schedule(self):
|
|
199
|
+
# Mock the _stop_running_schedule method
|
|
200
|
+
self.irrigation_service._stop_running_schedule = AsyncMock()
|
|
201
|
+
expected_response = {'data': {'result': 'stopped'}}
|
|
202
|
+
self.irrigation_service._stop_running_schedule.return_value = expected_response
|
|
203
|
+
|
|
204
|
+
# Test stopping running schedule
|
|
205
|
+
result = await self.irrigation_service.stop_running_schedule(self.test_irrigation)
|
|
206
|
+
|
|
207
|
+
# Verify the call was made with correct parameters
|
|
208
|
+
self.irrigation_service._stop_running_schedule.assert_awaited_once_with(
|
|
209
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/runningschedule",
|
|
210
|
+
self.test_irrigation,
|
|
211
|
+
"STOP"
|
|
212
|
+
)
|
|
213
|
+
self.assertEqual(result, expected_response)
|
|
214
|
+
|
|
215
|
+
async def test_update_device_props(self):
|
|
216
|
+
# Mock IoT properties response
|
|
217
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
218
|
+
'data': {
|
|
219
|
+
'props': {
|
|
220
|
+
'RSSI': '-70',
|
|
221
|
+
'IP': '192.168.1.101',
|
|
222
|
+
'sn': 'SN987654321',
|
|
223
|
+
'ssid': 'NewSSID',
|
|
224
|
+
'iot_state': 'connected'
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
updated_irrigation = await self.irrigation_service.update_device_props(self.test_irrigation)
|
|
230
|
+
|
|
231
|
+
# Test that properties were updated correctly
|
|
232
|
+
self.assertEqual(updated_irrigation.RSSI, '-70')
|
|
233
|
+
self.assertEqual(updated_irrigation.IP, '192.168.1.101')
|
|
234
|
+
self.assertEqual(updated_irrigation.sn, 'SN987654321')
|
|
235
|
+
self.assertEqual(updated_irrigation.ssid, 'NewSSID')
|
|
236
|
+
self.assertTrue(updated_irrigation.available)
|
|
237
|
+
|
|
238
|
+
async def test_update_device_props_disconnected(self):
|
|
239
|
+
# Mock IoT properties response with disconnected state
|
|
240
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
241
|
+
'data': {
|
|
242
|
+
'props': {
|
|
243
|
+
'RSSI': '-80',
|
|
244
|
+
'IP': '192.168.1.102',
|
|
245
|
+
'sn': 'SN555666777',
|
|
246
|
+
'ssid': 'TestSSID2',
|
|
247
|
+
'iot_state': 'disconnected'
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
updated_irrigation = await self.irrigation_service.update_device_props(self.test_irrigation)
|
|
253
|
+
|
|
254
|
+
# Test that device is marked as unavailable
|
|
255
|
+
self.assertFalse(updated_irrigation.available)
|
|
256
|
+
self.assertEqual(updated_irrigation.RSSI, '-80')
|
|
257
|
+
self.assertEqual(updated_irrigation.IP, '192.168.1.102')
|
|
258
|
+
|
|
259
|
+
async def test_get_iot_prop(self):
|
|
260
|
+
# Mock the get_iot_prop method directly to test the public interface
|
|
261
|
+
expected_response = {'data': {'props': {'RSSI': '-65'}}}
|
|
262
|
+
self.irrigation_service.get_iot_prop.return_value = expected_response
|
|
263
|
+
|
|
264
|
+
# Test get_iot_prop
|
|
265
|
+
result = await self.irrigation_service.get_iot_prop(self.test_irrigation)
|
|
266
|
+
|
|
267
|
+
# Verify the call was made and returned expected result
|
|
268
|
+
self.irrigation_service.get_iot_prop.assert_awaited_once_with(self.test_irrigation)
|
|
269
|
+
self.assertEqual(result, expected_response)
|
|
270
|
+
|
|
271
|
+
async def test_get_device_info(self):
|
|
272
|
+
# Mock the _irrigation_device_info method
|
|
273
|
+
self.irrigation_service._irrigation_device_info = AsyncMock()
|
|
274
|
+
expected_response = {'data': {'props': {'enable_schedules': True}}}
|
|
275
|
+
self.irrigation_service._irrigation_device_info.return_value = expected_response
|
|
276
|
+
|
|
277
|
+
# Test get_device_info
|
|
278
|
+
result = await self.irrigation_service.get_device_info(self.test_irrigation)
|
|
279
|
+
|
|
280
|
+
# Verify the call was made with correct parameters
|
|
281
|
+
expected_keys = 'wiring,sensor,enable_schedules,notification_enable,notification_watering_begins,notification_watering_ends,notification_watering_is_skipped,skip_low_temp,skip_wind,skip_rain,skip_saturation'
|
|
282
|
+
self.irrigation_service._irrigation_device_info.assert_awaited_once_with(
|
|
283
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/device_info",
|
|
284
|
+
self.test_irrigation,
|
|
285
|
+
expected_keys
|
|
286
|
+
)
|
|
287
|
+
self.assertEqual(result, expected_response)
|
|
288
|
+
|
|
289
|
+
async def test_get_zone_by_device_method(self):
|
|
290
|
+
# Mock the get_zone_by_device method directly to test the public interface
|
|
291
|
+
expected_response = {'data': {'zones': [{'zone_number': 1, 'name': 'Zone 1'}]}}
|
|
292
|
+
self.irrigation_service.get_zone_by_device.return_value = expected_response
|
|
293
|
+
|
|
294
|
+
# Test get_zone_by_device
|
|
295
|
+
result = await self.irrigation_service.get_zone_by_device(self.test_irrigation)
|
|
296
|
+
|
|
297
|
+
# Verify the call was made and returned expected result
|
|
298
|
+
self.irrigation_service.get_zone_by_device.assert_awaited_once_with(self.test_irrigation)
|
|
299
|
+
self.assertEqual(result, expected_response)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class TestZone(unittest.TestCase):
|
|
303
|
+
def test_zone_initialization_with_defaults(self):
|
|
304
|
+
# Test zone initialization with minimal data
|
|
305
|
+
zone_data = {'zone_number': 3, 'name': 'Test Zone'}
|
|
306
|
+
zone = Zone(zone_data)
|
|
307
|
+
|
|
308
|
+
self.assertEqual(zone.zone_number, 3)
|
|
309
|
+
self.assertEqual(zone.name, 'Test Zone')
|
|
310
|
+
self.assertTrue(zone.enabled) # Default value
|
|
311
|
+
self.assertEqual(zone.zone_id, 'zone_id') # Default value
|
|
312
|
+
self.assertEqual(zone.smart_duration, 600) # Default value
|
|
313
|
+
self.assertEqual(zone.quickrun_duration, 600) # Default value from smart_duration
|
|
314
|
+
|
|
315
|
+
def test_zone_initialization_with_all_data(self):
|
|
316
|
+
# Test zone initialization with all data
|
|
317
|
+
zone_data = {
|
|
318
|
+
'zone_number': 2,
|
|
319
|
+
'name': 'Garden Zone',
|
|
320
|
+
'enabled': False,
|
|
321
|
+
'zone_id': 'zone_garden',
|
|
322
|
+
'smart_duration': 1200
|
|
323
|
+
}
|
|
324
|
+
zone = Zone(zone_data)
|
|
325
|
+
|
|
326
|
+
self.assertEqual(zone.zone_number, 2)
|
|
327
|
+
self.assertEqual(zone.name, 'Garden Zone')
|
|
328
|
+
self.assertFalse(zone.enabled)
|
|
329
|
+
self.assertEqual(zone.zone_id, 'zone_garden')
|
|
330
|
+
self.assertEqual(zone.smart_duration, 1200)
|
|
331
|
+
self.assertEqual(zone.quickrun_duration, 1200) # Should use smart_duration
|
|
332
|
+
|
|
333
|
+
def test_zone_initialization_empty_dict(self):
|
|
334
|
+
# Test zone initialization with empty dict
|
|
335
|
+
zone = Zone({})
|
|
336
|
+
|
|
337
|
+
self.assertEqual(zone.zone_number, 1) # Default value
|
|
338
|
+
self.assertEqual(zone.name, 'Zone 1') # Default value
|
|
339
|
+
self.assertTrue(zone.enabled) # Default value
|
|
340
|
+
self.assertEqual(zone.zone_id, 'zone_id') # Default value
|
|
341
|
+
self.assertEqual(zone.smart_duration, 600) # Default value
|
|
342
|
+
self.assertEqual(zone.quickrun_duration, 600) # Default value
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class TestIrrigation(unittest.TestCase):
|
|
346
|
+
def test_irrigation_initialization(self):
|
|
347
|
+
# Test irrigation device initialization
|
|
348
|
+
irrigation_data = {
|
|
349
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
350
|
+
"product_model": "BS_WK1",
|
|
351
|
+
"mac": "IRRIG456",
|
|
352
|
+
"nickname": "Backyard Sprinkler",
|
|
353
|
+
"device_params": {"ip": "192.168.1.200"}
|
|
354
|
+
}
|
|
355
|
+
irrigation = Irrigation(irrigation_data)
|
|
356
|
+
|
|
357
|
+
self.assertEqual(irrigation.product_model, "BS_WK1")
|
|
358
|
+
self.assertEqual(irrigation.mac, "IRRIG456")
|
|
359
|
+
self.assertEqual(irrigation.nickname, "Backyard Sprinkler")
|
|
360
|
+
|
|
361
|
+
# Test default values
|
|
362
|
+
self.assertEqual(irrigation.RSSI, 0)
|
|
363
|
+
self.assertEqual(irrigation.IP, "192.168.1.100")
|
|
364
|
+
self.assertEqual(irrigation.sn, "SN123456789")
|
|
365
|
+
self.assertFalse(irrigation.available)
|
|
366
|
+
self.assertEqual(irrigation.ssid, "ssid")
|
|
367
|
+
self.assertEqual(len(irrigation.zones), 0)
|
|
368
|
+
|
|
369
|
+
def test_irrigation_inheritance(self):
|
|
370
|
+
# Test that Irrigation inherits from Device
|
|
371
|
+
irrigation_data = {
|
|
372
|
+
"product_type": DeviceTypes.IRRIGATION.value,
|
|
373
|
+
"product_model": "BS_WK1",
|
|
374
|
+
"mac": "IRRIG789",
|
|
375
|
+
"nickname": "Front Yard Sprinkler"
|
|
376
|
+
}
|
|
377
|
+
irrigation = Irrigation(irrigation_data)
|
|
378
|
+
|
|
379
|
+
# Test inherited Device properties
|
|
380
|
+
self.assertIsInstance(irrigation, Device)
|
|
381
|
+
self.assertEqual(irrigation.type, DeviceTypes.IRRIGATION)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class TestIrrigationServiceEdgeCases(unittest.IsolatedAsyncioTestCase):
|
|
385
|
+
async def asyncSetUp(self):
|
|
386
|
+
self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
|
|
387
|
+
self.irrigation_service = IrrigationService(auth_lib=self.mock_auth_lib)
|
|
388
|
+
self.irrigation_service.get_iot_prop = AsyncMock()
|
|
389
|
+
self.irrigation_service.get_zone_by_device = AsyncMock()
|
|
390
|
+
self.irrigation_service.get_object_list = AsyncMock()
|
|
391
|
+
|
|
392
|
+
# Create test irrigation
|
|
393
|
+
self.test_irrigation = Irrigation({
|
|
394
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
395
|
+
"product_model": "BS_WK1",
|
|
396
|
+
"mac": "IRRIG123",
|
|
397
|
+
"nickname": "Test Irrigation",
|
|
398
|
+
"device_params": {"ip": "192.168.1.100"},
|
|
399
|
+
"raw_dict": {}
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
async def test_update_with_empty_zones(self):
|
|
403
|
+
# Mock IoT properties response
|
|
404
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
405
|
+
'data': {
|
|
406
|
+
'props': {
|
|
407
|
+
'RSSI': '-65',
|
|
408
|
+
'IP': '192.168.1.100',
|
|
409
|
+
'sn': 'SN123456789',
|
|
410
|
+
'ssid': 'TestSSID',
|
|
411
|
+
'iot_state': 'connected'
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# Mock empty zones response
|
|
417
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
418
|
+
'data': {
|
|
419
|
+
'zones': []
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
424
|
+
|
|
425
|
+
# Verify empty zones list
|
|
426
|
+
self.assertEqual(len(updated_irrigation.zones), 0)
|
|
427
|
+
self.assertTrue(updated_irrigation.available)
|
|
428
|
+
|
|
429
|
+
async def test_update_with_missing_iot_props(self):
|
|
430
|
+
# Mock IoT properties response with missing props
|
|
431
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
432
|
+
'data': {
|
|
433
|
+
'props': {}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
438
|
+
'data': {
|
|
439
|
+
'zones': []
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
444
|
+
|
|
445
|
+
# Verify default values are used
|
|
446
|
+
self.assertEqual(updated_irrigation.RSSI, -65)
|
|
447
|
+
self.assertEqual(updated_irrigation.IP, '192.168.1.100')
|
|
448
|
+
self.assertEqual(updated_irrigation.sn, 'SN123456789')
|
|
449
|
+
self.assertEqual(updated_irrigation.ssid, 'ssid')
|
|
450
|
+
self.assertFalse(updated_irrigation.available) # iot_state missing, so not connected
|
|
451
|
+
|
|
452
|
+
async def test_get_irrigations_empty_device_list(self):
|
|
453
|
+
# Mock empty device list
|
|
454
|
+
self.irrigation_service.get_object_list.return_value = []
|
|
455
|
+
|
|
456
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
457
|
+
|
|
458
|
+
# Verify empty list returned
|
|
459
|
+
self.assertEqual(len(irrigations), 0)
|
|
460
|
+
self.irrigation_service.get_object_list.assert_awaited_once()
|
|
461
|
+
|
|
462
|
+
async def test_get_irrigations_no_irrigation_devices(self):
|
|
463
|
+
# Mock device list with non-irrigation devices
|
|
464
|
+
mock_camera = MagicMock()
|
|
465
|
+
mock_camera.type = DeviceTypes.CAMERA
|
|
466
|
+
mock_camera.product_model = "CAM_V1"
|
|
467
|
+
|
|
468
|
+
mock_bulb = MagicMock()
|
|
469
|
+
mock_bulb.type = DeviceTypes.LIGHT
|
|
470
|
+
mock_bulb.product_model = "LIGHT_V1"
|
|
471
|
+
|
|
472
|
+
self.irrigation_service.get_object_list.return_value = [mock_camera, mock_bulb]
|
|
473
|
+
|
|
474
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
475
|
+
|
|
476
|
+
# Verify no irrigation devices returned
|
|
477
|
+
self.assertEqual(len(irrigations), 0)
|
|
478
|
+
|
|
479
|
+
async def test_get_irrigations_wrong_product_model(self):
|
|
480
|
+
# Mock device list with irrigation type but wrong product model
|
|
481
|
+
mock_irrigation = MagicMock()
|
|
482
|
+
mock_irrigation.type = DeviceTypes.IRRIGATION
|
|
483
|
+
mock_irrigation.product_model = "WRONG_MODEL"
|
|
484
|
+
mock_irrigation.raw_dict = {
|
|
485
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
486
|
+
"product_model": "WRONG_MODEL",
|
|
487
|
+
"mac": "IRRIG123",
|
|
488
|
+
"nickname": "Test Irrigation"
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
self.irrigation_service.get_object_list.return_value = [mock_irrigation]
|
|
492
|
+
|
|
493
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
494
|
+
|
|
495
|
+
# Verify no irrigation devices returned due to wrong product model
|
|
496
|
+
self.assertEqual(len(irrigations), 0)
|
|
497
|
+
|
|
498
|
+
async def test_set_zone_quickrun_duration_zone_not_found(self):
|
|
499
|
+
# Setup test irrigation with zones
|
|
500
|
+
self.test_irrigation.zones = [
|
|
501
|
+
Zone({
|
|
502
|
+
'zone_number': 1,
|
|
503
|
+
'name': 'Zone 1',
|
|
504
|
+
'enabled': True,
|
|
505
|
+
'zone_id': 'zone1',
|
|
506
|
+
'smart_duration': 600
|
|
507
|
+
})
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
# Try to set duration for non-existent zone
|
|
511
|
+
result = await self.irrigation_service.set_zone_quickrun_duration(
|
|
512
|
+
self.test_irrigation,
|
|
513
|
+
99, # Non-existent zone
|
|
514
|
+
300
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Verify existing zone unchanged
|
|
518
|
+
self.assertEqual(self.test_irrigation.zones[0].quickrun_duration, 600)
|
|
519
|
+
self.assertEqual(result, self.test_irrigation)
|
|
520
|
+
|
|
521
|
+
async def test_set_zone_quickrun_duration_no_zones(self):
|
|
522
|
+
# Test with irrigation that has no zones
|
|
523
|
+
self.test_irrigation.zones = []
|
|
524
|
+
|
|
525
|
+
result = await self.irrigation_service.set_zone_quickrun_duration(
|
|
526
|
+
self.test_irrigation,
|
|
527
|
+
1,
|
|
528
|
+
300
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# Verify no error and empty zones list
|
|
532
|
+
self.assertEqual(len(self.test_irrigation.zones), 0)
|
|
533
|
+
self.assertEqual(result, self.test_irrigation)
|
|
534
|
+
|
|
535
|
+
if __name__ == '__main__':
|
|
536
|
+
unittest.main()
|
wyzeapy/types.py
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
# of the attached license. You should have received a copy of
|
|
4
4
|
# the license with this file. If not, please write to:
|
|
5
5
|
# katie@mulliken.net to receive a copy
|
|
6
|
+
"""
|
|
7
|
+
Type definitions and data models for Wyzeapy library, including devices, events,
|
|
8
|
+
and API response code enums.
|
|
9
|
+
"""
|
|
10
|
+
|
|
6
11
|
from enum import Enum
|
|
7
12
|
from typing import Union, List, Dict, Any
|
|
8
13
|
|
|
@@ -43,6 +48,7 @@ class DeviceTypes(Enum):
|
|
|
43
48
|
SENSE_V2_GATEWAY = "S1Gateway"
|
|
44
49
|
KEYPAD = "Keypad"
|
|
45
50
|
LIGHTSTRIP = "LightStrip"
|
|
51
|
+
IRRIGATION = "Common"
|
|
46
52
|
|
|
47
53
|
|
|
48
54
|
class Device:
|
|
@@ -79,15 +85,17 @@ class Sensor(Device):
|
|
|
79
85
|
@property
|
|
80
86
|
def activity_detected(self) -> int:
|
|
81
87
|
if self.type is DeviceTypes.CONTACT_SENSOR:
|
|
82
|
-
return int(self.device_params[
|
|
88
|
+
return int(self.device_params["open_close_state"])
|
|
83
89
|
elif self.type is DeviceTypes.MOTION_SENSOR:
|
|
84
|
-
return int(self.device_params[
|
|
90
|
+
return int(self.device_params["motion_state"])
|
|
85
91
|
else:
|
|
86
|
-
raise AssertionError(
|
|
92
|
+
raise AssertionError(
|
|
93
|
+
"Device must be of type CONTACT_SENSOR or MOTION_SENSOR"
|
|
94
|
+
)
|
|
87
95
|
|
|
88
96
|
@property
|
|
89
97
|
def is_low_battery(self) -> int:
|
|
90
|
-
return int(self.device_params[
|
|
98
|
+
return int(self.device_params["is_low_battery"])
|
|
91
99
|
|
|
92
100
|
|
|
93
101
|
class PropertyIDs(Enum):
|
|
@@ -104,11 +112,11 @@ class PropertyIDs(Enum):
|
|
|
104
112
|
CONTACT_STATE = "P1301"
|
|
105
113
|
MOTION_STATE = "P1302"
|
|
106
114
|
CAMERA_SIREN = "P1049"
|
|
107
|
-
ACCESSORY = "P1056"
|
|
115
|
+
ACCESSORY = "P1056" # Is state for camera accessories, like garage doors, light sockets, and floodlights.
|
|
108
116
|
SUN_MATCH = "P1528"
|
|
109
117
|
MOTION_DETECTION = "P1047" # Current Motion Detection State of the Camera
|
|
110
118
|
MOTION_DETECTION_TOGGLE = "P1001" # This toggles Camera Motion Detection On/Off
|
|
111
|
-
WCO_MOTION_DETECTION = "P1029"
|
|
119
|
+
WCO_MOTION_DETECTION = "P1029" # Wyze cam outdoor requires both P1047 and P1029 to be set. P1029 is set via set_property_list
|
|
112
120
|
|
|
113
121
|
|
|
114
122
|
class WallSwitchProps(Enum):
|
|
@@ -149,11 +157,19 @@ class ThermostatProps(Enum):
|
|
|
149
157
|
ASW_HOLD = "asw_hold"
|
|
150
158
|
|
|
151
159
|
|
|
160
|
+
class IrrigationProps(Enum):
|
|
161
|
+
IOT_STATE = "iot_state" # Connection state: connected, disconnected
|
|
162
|
+
RSSI = "RSSI"
|
|
163
|
+
IP = "IP"
|
|
164
|
+
SN = "sn"
|
|
165
|
+
SSID = "ssid"
|
|
166
|
+
|
|
167
|
+
|
|
152
168
|
class ResponseCodes(Enum):
|
|
153
169
|
SUCCESS = "1"
|
|
154
170
|
PARAMETER_ERROR = "1001"
|
|
155
171
|
ACCESS_TOKEN_ERROR = "2001"
|
|
156
|
-
DEVICE_OFFLINE =
|
|
172
|
+
DEVICE_OFFLINE = "3019"
|
|
157
173
|
|
|
158
174
|
|
|
159
175
|
class ResponseCodesLock(Enum):
|
|
@@ -205,9 +221,9 @@ class Event:
|
|
|
205
221
|
|
|
206
222
|
|
|
207
223
|
class HMSStatus(Enum):
|
|
208
|
-
DISARMED =
|
|
209
|
-
HOME =
|
|
210
|
-
AWAY =
|
|
224
|
+
DISARMED = "disarmed"
|
|
225
|
+
HOME = "home"
|
|
226
|
+
AWAY = "away"
|
|
211
227
|
|
|
212
228
|
|
|
213
229
|
class DeviceMgmtToggleType:
|
|
@@ -217,6 +233,7 @@ class DeviceMgmtToggleType:
|
|
|
217
233
|
|
|
218
234
|
|
|
219
235
|
class DeviceMgmtToggleProps(Enum):
|
|
220
|
-
EVENT_RECORDING_TOGGLE = DeviceMgmtToggleType(
|
|
236
|
+
EVENT_RECORDING_TOGGLE = DeviceMgmtToggleType(
|
|
237
|
+
"cam_event_recording", "ge.motion_detect_recording"
|
|
238
|
+
)
|
|
221
239
|
NOTIFICATION_TOGGLE = DeviceMgmtToggleType("cam_device_notify", "ge.push_switch")
|
|
222
|
-
|