wyzeapy 0.5.29__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 +10 -0
- wyzeapy/payload_factory.py +30 -0
- wyzeapy/services/base_service.py +73 -0
- wyzeapy/services/irrigation_service.py +189 -0
- wyzeapy/tests/test_irrigation_service.py +536 -0
- wyzeapy/types.py +9 -0
- {wyzeapy-0.5.29.dist-info → wyzeapy-0.5.30.dist-info}/METADATA +1 -1
- {wyzeapy-0.5.29.dist-info → wyzeapy-0.5.30.dist-info}/RECORD +9 -7
- {wyzeapy-0.5.29.dist-info → wyzeapy-0.5.30.dist-info}/WHEEL +0 -0
wyzeapy/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ from .services.lock_service import LockService
|
|
|
16
16
|
from .services.sensor_service import SensorService
|
|
17
17
|
from .services.switch_service import SwitchService, SwitchUsageService
|
|
18
18
|
from .services.thermostat_service import ThermostatService
|
|
19
|
+
from .services.irrigation_service import IrrigationService
|
|
19
20
|
from .services.wall_switch_service import WallSwitchService
|
|
20
21
|
from .wyze_auth_lib import WyzeAuthLib, Token
|
|
21
22
|
|
|
@@ -50,6 +51,7 @@ class Wyzeapy:
|
|
|
50
51
|
self._hms_service = None
|
|
51
52
|
self._lock_service = None
|
|
52
53
|
self._sensor_service = None
|
|
54
|
+
self._irrigation_service = None
|
|
53
55
|
self._wall_switch_service = None
|
|
54
56
|
self._switch_usage_service = None
|
|
55
57
|
self._email = None
|
|
@@ -441,6 +443,14 @@ class Wyzeapy:
|
|
|
441
443
|
if self._sensor_service is None:
|
|
442
444
|
self._sensor_service = SensorService(self._auth_lib)
|
|
443
445
|
return self._sensor_service
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
async def irrigation_service(self) -> IrrigationService:
|
|
449
|
+
"""Returns an instance of the irrigation service"""
|
|
450
|
+
|
|
451
|
+
if self._irrigation_service is None:
|
|
452
|
+
self._irrigation_service = IrrigationService(self._auth_lib)
|
|
453
|
+
return self._irrigation_service
|
|
444
454
|
|
|
445
455
|
@property
|
|
446
456
|
async def wall_switch_service(self) -> WallSwitchService:
|
wyzeapy/payload_factory.py
CHANGED
|
@@ -51,6 +51,36 @@ def olive_create_get_payload(device_mac: str, keys: str) -> Dict[str, Any]:
|
|
|
51
51
|
|
|
52
52
|
return {"keys": keys, "did": device_mac, "nonce": nonce}
|
|
53
53
|
|
|
54
|
+
def olive_create_get_payload_irrigation(device_mac: str) -> Dict[str, Any]:
|
|
55
|
+
nonce = int(time.time() * 1000)
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
'device_id': device_mac,
|
|
59
|
+
'nonce': str(nonce)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
def olive_create_post_payload_irrigation_stop(device_mac: str, action: str) -> Dict[str, Any]:
|
|
63
|
+
nonce = int(time.time() * 1000)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
'device_id': device_mac,
|
|
67
|
+
'nonce': str(nonce),
|
|
68
|
+
"action": action
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def olive_create_post_payload_irrigation_quickrun(device_mac: str, zone_number: int, duration: int) -> Dict[str, Any]:
|
|
72
|
+
nonce = int(time.time() * 1000)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
'device_id': device_mac,
|
|
76
|
+
'nonce': str(nonce),
|
|
77
|
+
"zone_runs": [
|
|
78
|
+
{
|
|
79
|
+
"zone_number": zone_number,
|
|
80
|
+
"duration": duration
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
|
54
84
|
|
|
55
85
|
def olive_create_post_payload(
|
|
56
86
|
device_mac: str, device_model: str, prop_key: str, value: Any
|
wyzeapy/services/base_service.py
CHANGED
|
@@ -36,6 +36,9 @@ from ..payload_factory import (
|
|
|
36
36
|
olive_create_user_info_payload,
|
|
37
37
|
devicemgmt_create_capabilities_payload,
|
|
38
38
|
devicemgmt_get_iot_props_list,
|
|
39
|
+
olive_create_get_payload_irrigation,
|
|
40
|
+
olive_create_post_payload_irrigation_stop,
|
|
41
|
+
olive_create_post_payload_irrigation_quickrun,
|
|
39
42
|
)
|
|
40
43
|
from ..types import PropertyIDs, Device, DeviceMgmtToggleType
|
|
41
44
|
from ..utils import (
|
|
@@ -896,3 +899,73 @@ class BaseService:
|
|
|
896
899
|
check_for_errors_standard(self, response_json)
|
|
897
900
|
|
|
898
901
|
return response_json["data"]["usage_record_list"]
|
|
902
|
+
|
|
903
|
+
async def _get_zone_by_device(self, url: str, device: Device) -> Dict[Any, Any]:
|
|
904
|
+
await self._auth_lib.refresh_if_should()
|
|
905
|
+
|
|
906
|
+
payload = olive_create_get_payload_irrigation(device.mac)
|
|
907
|
+
signature = olive_create_signature(payload, self._auth_lib.token.access_token)
|
|
908
|
+
headers = {
|
|
909
|
+
'Accept-Encoding': 'gzip',
|
|
910
|
+
'User-Agent': 'myapp',
|
|
911
|
+
'appid': OLIVE_APP_ID,
|
|
912
|
+
'appinfo': APP_INFO,
|
|
913
|
+
'phoneid': PHONE_ID,
|
|
914
|
+
'access_token': self._auth_lib.token.access_token,
|
|
915
|
+
'signature2': signature
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
response_json = await self._auth_lib.get(url, headers=headers, params=payload)
|
|
919
|
+
|
|
920
|
+
check_for_errors_iot(self, response_json)
|
|
921
|
+
|
|
922
|
+
return response_json
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
async def _stop_running_schedule(self, url: str, device: Device, action: str) -> Dict[Any, Any]:
|
|
926
|
+
await self._auth_lib.refresh_if_should()
|
|
927
|
+
|
|
928
|
+
payload = olive_create_post_payload_irrigation_stop(device.mac, action)
|
|
929
|
+
signature = olive_create_signature(json.dumps(payload, separators=(',', ':')),
|
|
930
|
+
self._auth_lib.token.access_token)
|
|
931
|
+
headers = {
|
|
932
|
+
'Accept-Encoding': 'gzip',
|
|
933
|
+
'Content-Type': 'application/json',
|
|
934
|
+
'User-Agent': 'myapp',
|
|
935
|
+
'appid': OLIVE_APP_ID,
|
|
936
|
+
'appinfo': APP_INFO,
|
|
937
|
+
'phoneid': PHONE_ID,
|
|
938
|
+
'access_token': self._auth_lib.token.access_token,
|
|
939
|
+
'signature2': signature
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
payload_str = json.dumps(payload, separators=(',', ':'))
|
|
943
|
+
response_json = await self._auth_lib.post(url, headers=headers, data=payload_str)
|
|
944
|
+
|
|
945
|
+
check_for_errors_iot(self, response_json)
|
|
946
|
+
|
|
947
|
+
return response_json
|
|
948
|
+
|
|
949
|
+
async def _start_zone(self, url: str, device: Device, zone_number: int, duration: int) -> Dict[Any, Any]:
|
|
950
|
+
await self._auth_lib.refresh_if_should()
|
|
951
|
+
|
|
952
|
+
payload = olive_create_post_payload_irrigation_quickrun(device.mac, zone_number, duration)
|
|
953
|
+
signature = olive_create_signature(json.dumps(payload, separators=(',', ':')),
|
|
954
|
+
self._auth_lib.token.access_token)
|
|
955
|
+
headers = {
|
|
956
|
+
'Accept-Encoding': 'gzip',
|
|
957
|
+
'Content-Type': 'application/json',
|
|
958
|
+
'User-Agent': 'myapp',
|
|
959
|
+
'appid': OLIVE_APP_ID,
|
|
960
|
+
'appinfo': APP_INFO,
|
|
961
|
+
'phoneid': PHONE_ID,
|
|
962
|
+
'access_token': self._auth_lib.token.access_token,
|
|
963
|
+
'signature2': signature
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
payload_str = json.dumps(payload, separators=(',', ':'))
|
|
967
|
+
response_json = await self._auth_lib.post(url, headers=headers, data=payload_str)
|
|
968
|
+
|
|
969
|
+
check_for_errors_iot(self, response_json)
|
|
970
|
+
|
|
971
|
+
return response_json
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from .base_service import BaseService
|
|
6
|
+
from ..types import Device, IrrigationProps, DeviceTypes
|
|
7
|
+
|
|
8
|
+
_LOGGER = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CropType(Enum):
|
|
12
|
+
COOL_SEASON_GRASS = "cool_season_grass"
|
|
13
|
+
WARM_SEASON_GRASS = "warm_season_grass"
|
|
14
|
+
SHRUBS = "shrubs"
|
|
15
|
+
TREES = "trees"
|
|
16
|
+
ANNUALS = "annuals"
|
|
17
|
+
PERENNIALS = "perennials"
|
|
18
|
+
XERISCAPE = "xeriscape"
|
|
19
|
+
GARDEN = "garden"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ExposureType(Enum):
|
|
23
|
+
LOTS_OF_SUN = "lots_of_sun"
|
|
24
|
+
SOME_SHADE = "some_shade"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NozzleType(Enum):
|
|
28
|
+
FIXED_SPRAY_HEAD = "fixed_spray_head"
|
|
29
|
+
ROTOR_HEAD = "rotor_head"
|
|
30
|
+
ROTARY_NOZZLE = "rotary_nozzle"
|
|
31
|
+
MISTER = "mister"
|
|
32
|
+
BUBBLER = "bubbler"
|
|
33
|
+
EMITTER = "emitter"
|
|
34
|
+
DRIP_LINE = "drip_line"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SlopeType(Enum):
|
|
38
|
+
FLAT = "flat"
|
|
39
|
+
SLIGHT = "slight"
|
|
40
|
+
MODERATE = "moderate"
|
|
41
|
+
STEEP = "steep"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SoilType(Enum):
|
|
45
|
+
CLAY_LOAM = 'clay_loam'
|
|
46
|
+
CLAY = 'clay'
|
|
47
|
+
SILTY_CLAY = 'silty_clay'
|
|
48
|
+
LOAM = 'loam'
|
|
49
|
+
SANDY_LOAM = 'sandy_loam'
|
|
50
|
+
LOAMY_SAND = 'loamy_sand'
|
|
51
|
+
SAND = 'sand'
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Zone:
|
|
55
|
+
"""Represents a single irrigation zone."""
|
|
56
|
+
def __init__(self, dictionary: Dict[Any, Any]):
|
|
57
|
+
self.zone_number: int = dictionary.get('zone_number', 1)
|
|
58
|
+
self.name: str = dictionary.get('name', 'Zone 1')
|
|
59
|
+
self.enabled: bool = dictionary.get('enabled', True)
|
|
60
|
+
self.zone_id: str = dictionary.get('zone_id', 'zone_id')
|
|
61
|
+
self.smart_duration: int = dictionary.get('smart_duration', 600)
|
|
62
|
+
|
|
63
|
+
# this quickrun duration is used only for running a zone manually
|
|
64
|
+
# the wyze api has no such value, but takes a duration as part of the api call
|
|
65
|
+
# the default value grabs the wyze smart_duration but all further updates
|
|
66
|
+
# are managed through the home assistant state
|
|
67
|
+
self.quickrun_duration: int = dictionary.get('smart_duration', 600)
|
|
68
|
+
|
|
69
|
+
class Irrigation(Device):
|
|
70
|
+
def __init__(self, dictionary: Dict[Any, Any]):
|
|
71
|
+
super().__init__(dictionary)
|
|
72
|
+
|
|
73
|
+
# the below comes from the get_iot_prop call
|
|
74
|
+
self.RSSI: int = 0
|
|
75
|
+
self.IP: str = "192.168.1.100"
|
|
76
|
+
self.sn: str = "SN123456789"
|
|
77
|
+
self.available: bool = False
|
|
78
|
+
self.ssid: str = "ssid"
|
|
79
|
+
# the below comes from the device_info call
|
|
80
|
+
self.zones: List[Zone] = []
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class IrrigationService(BaseService):
|
|
84
|
+
async def update(self, irrigation: Irrigation) -> Irrigation:
|
|
85
|
+
"""Update the irrigation device with latest data from Wyze API."""
|
|
86
|
+
# Get IoT properties
|
|
87
|
+
properties = (await self.get_iot_prop(irrigation))['data']['props']
|
|
88
|
+
|
|
89
|
+
# Update device properties
|
|
90
|
+
irrigation.RSSI = properties.get('RSSI', -65)
|
|
91
|
+
irrigation.IP = properties.get('IP', '192.168.1.100')
|
|
92
|
+
irrigation.sn = properties.get('sn', 'SN123456789')
|
|
93
|
+
irrigation.ssid = properties.get('ssid', 'ssid')
|
|
94
|
+
irrigation.available = (properties.get(IrrigationProps.IOT_STATE.value) == "connected")
|
|
95
|
+
|
|
96
|
+
# Get zones
|
|
97
|
+
zones = (await self.get_zone_by_device(irrigation))['data']['zones']
|
|
98
|
+
|
|
99
|
+
# Update zones
|
|
100
|
+
irrigation.zones = []
|
|
101
|
+
for zone in zones:
|
|
102
|
+
irrigation.zones.append(Zone(zone))
|
|
103
|
+
|
|
104
|
+
return irrigation
|
|
105
|
+
|
|
106
|
+
async def update_device_props(self, irrigation: Irrigation) -> Irrigation:
|
|
107
|
+
"""Update the irrigation device with latest data from Wyze API."""
|
|
108
|
+
# Get IoT properties
|
|
109
|
+
properties = (await self.get_iot_prop(irrigation))['data']['props']
|
|
110
|
+
|
|
111
|
+
# Update device properties
|
|
112
|
+
irrigation.RSSI = properties.get('RSSI')
|
|
113
|
+
irrigation.IP = properties.get('IP')
|
|
114
|
+
irrigation.sn = properties.get('sn')
|
|
115
|
+
irrigation.ssid = properties.get('ssid')
|
|
116
|
+
irrigation.available = (properties.get(IrrigationProps.IOT_STATE.value) == 'connected')
|
|
117
|
+
|
|
118
|
+
return irrigation
|
|
119
|
+
|
|
120
|
+
async def get_irrigations(self) -> List[Irrigation]:
|
|
121
|
+
if self._devices is None:
|
|
122
|
+
self._devices = await self.get_object_list()
|
|
123
|
+
|
|
124
|
+
irrigations = [device for device in self._devices if device.type == DeviceTypes.IRRIGATION and "BS_WK1" in device.product_model]
|
|
125
|
+
|
|
126
|
+
return [Irrigation(irrigation.raw_dict) for irrigation in irrigations]
|
|
127
|
+
|
|
128
|
+
async def start_zone(self, irrigation: Device, zone_number: int, quickrun_duration: int) -> Dict[Any, Any]:
|
|
129
|
+
"""Start a zone with the specified duration.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
irrigation: The irrigation device
|
|
133
|
+
zone_number: The zone number to start
|
|
134
|
+
quickrun_duration: Duration in seconds to run the zone
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dict containing the API response
|
|
138
|
+
"""
|
|
139
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/quickrun"
|
|
140
|
+
return await self._start_zone(url, irrigation, zone_number, quickrun_duration)
|
|
141
|
+
|
|
142
|
+
async def stop_running_schedule(self, device: Device) -> Dict[Any, Any]:
|
|
143
|
+
"""Stop any currently running irrigation schedule.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
device: The irrigation device
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Dict containing the API response
|
|
150
|
+
"""
|
|
151
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/runningschedule"
|
|
152
|
+
action = "STOP"
|
|
153
|
+
return await self._stop_running_schedule(url, device, action)
|
|
154
|
+
|
|
155
|
+
async def set_zone_quickrun_duration(self, irrigation: Irrigation, zone_number: int, duration: int) -> Irrigation:
|
|
156
|
+
"""Set the quickrun duration for a specific zone.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
irrigation: The irrigation device
|
|
160
|
+
zone_number: The zone number to configure
|
|
161
|
+
duration: Duration in seconds for quickrun
|
|
162
|
+
"""
|
|
163
|
+
for zone in irrigation.zones:
|
|
164
|
+
if zone.zone_number == zone_number:
|
|
165
|
+
zone.quickrun_duration = duration
|
|
166
|
+
break
|
|
167
|
+
|
|
168
|
+
return irrigation
|
|
169
|
+
|
|
170
|
+
# Private implementation methods
|
|
171
|
+
async def get_iot_prop(self, device: Device) -> Dict[Any, Any]:
|
|
172
|
+
"""Get IoT properties for a device."""
|
|
173
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/get_iot_prop"
|
|
174
|
+
keys = 'zone_state,iot_state,iot_state_update_time,app_version,RSSI,' \
|
|
175
|
+
'wifi_mac,sn,device_model,ssid,IP'
|
|
176
|
+
return await self._get_iot_prop(url, device, keys)
|
|
177
|
+
|
|
178
|
+
async def get_device_info(self, device: Device) -> Dict[Any, Any]:
|
|
179
|
+
"""Get device info from Wyze API."""
|
|
180
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/device_info"
|
|
181
|
+
keys = 'wiring,sensor,enable_schedules,notification_enable,notification_watering_begins,' \
|
|
182
|
+
'notification_watering_ends,notification_watering_is_skipped,skip_low_temp,skip_wind,' \
|
|
183
|
+
'skip_rain,skip_saturation'
|
|
184
|
+
return await self._irrigation_device_info(url, device, keys)
|
|
185
|
+
|
|
186
|
+
async def get_zone_by_device(self, device: Device) -> List[Dict[Any, Any]]:
|
|
187
|
+
"""Get zones for a device."""
|
|
188
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/zone"
|
|
189
|
+
return await self._get_zone_by_device(url, device)
|
|
@@ -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
|
@@ -48,6 +48,7 @@ class DeviceTypes(Enum):
|
|
|
48
48
|
SENSE_V2_GATEWAY = "S1Gateway"
|
|
49
49
|
KEYPAD = "Keypad"
|
|
50
50
|
LIGHTSTRIP = "LightStrip"
|
|
51
|
+
IRRIGATION = "Common"
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
class Device:
|
|
@@ -156,6 +157,14 @@ class ThermostatProps(Enum):
|
|
|
156
157
|
ASW_HOLD = "asw_hold"
|
|
157
158
|
|
|
158
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
|
+
|
|
159
168
|
class ResponseCodes(Enum):
|
|
160
169
|
SUCCESS = "1"
|
|
161
170
|
PARAMETER_ERROR = "1001"
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
wyzeapy/__init__.py,sha256=
|
|
1
|
+
wyzeapy/__init__.py,sha256=QckgsDmJxrQFPdVT4rheUqi3cCBSN-jkEWD-6hHmrqk,17248
|
|
2
2
|
wyzeapy/const.py,sha256=3PV2Uq7wDD1X0ZmJBg8GWXYGHrpJvszyr9FuvwjHyus,1249
|
|
3
3
|
wyzeapy/crypto.py,sha256=ApzPL0hrrd0D4k2jB5psNatJvUSzx1Kxui6_l4NJGO8,2057
|
|
4
4
|
wyzeapy/exceptions.py,sha256=uKVWooofK22DZ3o9kwxnlJXnhk0VMJXnp-26pNavAis,1136
|
|
5
|
-
wyzeapy/payload_factory.py,sha256=
|
|
6
|
-
wyzeapy/types.py,sha256=
|
|
5
|
+
wyzeapy/payload_factory.py,sha256=D37H0rTFakqnhmYrYjBAhFvRSquujINA7b8UyhRUjl0,20547
|
|
6
|
+
wyzeapy/types.py,sha256=LPLc86FkEy8rUoPMwG8xB73NCTVRkJ1O5w7uSfDydLQ,6785
|
|
7
7
|
wyzeapy/utils.py,sha256=EXnsZFBxgI3LsIh9Ttg4gq3Aq1VMLOxBUPCspWJX9IQ,7407
|
|
8
8
|
wyzeapy/wyze_auth_lib.py,sha256=CiWdl_UAzYxKLS8yCfjXxEI87gStJede2eS4g7KlnjE,18273
|
|
9
9
|
wyzeapy/services/__init__.py,sha256=hbdyglbWQjM4XlNqPIACOEbspdsEEm4k5VXZ1hI0gc8,77
|
|
10
|
-
wyzeapy/services/base_service.py,sha256=
|
|
10
|
+
wyzeapy/services/base_service.py,sha256=HUIU2uugHhY0KiGvc5ALYQn4NnsFMNZzECLJgqPHR9Q,34573
|
|
11
11
|
wyzeapy/services/bulb_service.py,sha256=DNuT9PBmFhXpaD9rcjoYZMt-TLWWEAC3o0Yyvw_itHA,7825
|
|
12
12
|
wyzeapy/services/camera_service.py,sha256=vaJIChKDMg3zWcoC_JqITDBjw4sgFdyUEkl3_w2ML8I,11170
|
|
13
13
|
wyzeapy/services/hms_service.py,sha256=lQojRASz9AlwqkRfj7W7gOKXpLHrHHVwBGMw5WJ23Nc,2450
|
|
14
|
+
wyzeapy/services/irrigation_service.py,sha256=lzQoUT2COlIMF1oTTDsvb-7H7zkra96i_Cxr4zkfYzs,7110
|
|
14
15
|
wyzeapy/services/lock_service.py,sha256=NBjlr7pL5zJqdJaH33v1i6BbLHb7TXCkoCql4hCr8J8,2234
|
|
15
16
|
wyzeapy/services/sensor_service.py,sha256=WSNz0OOLoZKru4d1ZwZ80-pdJ321HssUnwEgVfwX2zM,3578
|
|
16
17
|
wyzeapy/services/switch_service.py,sha256=2O3J8-hP3vOgGVi0cKiKG_3j71zI6rHiqQd3u7CEKcE,2244
|
|
17
18
|
wyzeapy/services/thermostat_service.py,sha256=_d-UbD65JArhwsslawvwpTmfVC4tMksY-L1Uu7HW0m4,5360
|
|
18
19
|
wyzeapy/services/update_manager.py,sha256=5pZJmnyN4rlYJwMtEY13NlPBssnRLhtlLLmXr9t990Q,6770
|
|
19
20
|
wyzeapy/services/wall_switch_service.py,sha256=cBKmnB2InHKIuoPwQ47t1rDtDplyOyGQYvnfX4fXFcc,4339
|
|
20
|
-
wyzeapy
|
|
21
|
-
wyzeapy-0.5.
|
|
22
|
-
wyzeapy-0.5.
|
|
21
|
+
wyzeapy/tests/test_irrigation_service.py,sha256=6rknfL0FzkptHs4CcYBamBTMD4pHcG0RyYNe3OgYBI8,21270
|
|
22
|
+
wyzeapy-0.5.30.dist-info/METADATA,sha256=7OVSrLycTNIfh3zcmTI-38EZ0xt4NZExNKamzuTT61I,445
|
|
23
|
+
wyzeapy-0.5.30.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
24
|
+
wyzeapy-0.5.30.dist-info/RECORD,,
|
|
File without changes
|