wyzeapy 0.5.29__py3-none-any.whl → 0.5.31__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 +34 -0
- wyzeapy/services/base_service.py +111 -0
- wyzeapy/services/bulb_service.py +25 -4
- wyzeapy/services/irrigation_service.py +246 -0
- wyzeapy/tests/test_irrigation_service.py +732 -0
- wyzeapy/types.py +9 -0
- wyzeapy/utils.py +14 -4
- {wyzeapy-0.5.29.dist-info → wyzeapy-0.5.31.dist-info}/METADATA +4 -4
- {wyzeapy-0.5.29.dist-info → wyzeapy-0.5.31.dist-info}/RECORD +11 -9
- {wyzeapy-0.5.29.dist-info → wyzeapy-0.5.31.dist-info}/WHEEL +1 -1
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
|
|
@@ -442,6 +444,14 @@ class Wyzeapy:
|
|
|
442
444
|
self._sensor_service = SensorService(self._auth_lib)
|
|
443
445
|
return self._sensor_service
|
|
444
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
|
|
454
|
+
|
|
445
455
|
@property
|
|
446
456
|
async def wall_switch_service(self) -> WallSwitchService:
|
|
447
457
|
"""Provides access to the Wyze Wall Switch service.
|
wyzeapy/payload_factory.py
CHANGED
|
@@ -52,6 +52,40 @@ def olive_create_get_payload(device_mac: str, keys: str) -> Dict[str, Any]:
|
|
|
52
52
|
return {"keys": keys, "did": device_mac, "nonce": nonce}
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
def olive_create_get_payload_irrigation(device_mac: str) -> Dict[str, Any]:
|
|
56
|
+
nonce = int(time.time() * 1000)
|
|
57
|
+
|
|
58
|
+
return {"device_id": device_mac, "nonce": str(nonce)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def olive_create_post_payload_irrigation_stop(
|
|
62
|
+
device_mac: str, action: str
|
|
63
|
+
) -> Dict[str, Any]:
|
|
64
|
+
nonce = int(time.time() * 1000)
|
|
65
|
+
|
|
66
|
+
return {"device_id": device_mac, "nonce": str(nonce), "action": action}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def olive_create_post_payload_irrigation_quickrun(
|
|
70
|
+
device_mac: str, zone_number: int, duration: int
|
|
71
|
+
) -> Dict[str, Any]:
|
|
72
|
+
nonce = int(time.time() * 1000)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"device_id": device_mac,
|
|
76
|
+
"nonce": str(nonce),
|
|
77
|
+
"zone_runs": [{"zone_number": zone_number, "duration": duration}],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def olive_create_get_payload_irrigation_schedule_runs(
|
|
82
|
+
device_mac: str,
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
nonce = int(time.time() * 1000)
|
|
85
|
+
|
|
86
|
+
return {"device_id": device_mac, "nonce": str(nonce)}
|
|
87
|
+
|
|
88
|
+
|
|
55
89
|
def olive_create_post_payload(
|
|
56
90
|
device_mac: str, device_model: str, prop_key: str, value: Any
|
|
57
91
|
) -> Dict[str, Any]:
|
wyzeapy/services/base_service.py
CHANGED
|
@@ -36,6 +36,10 @@ 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,
|
|
42
|
+
olive_create_get_payload_irrigation_schedule_runs,
|
|
39
43
|
)
|
|
40
44
|
from ..types import PropertyIDs, Device, DeviceMgmtToggleType
|
|
41
45
|
from ..utils import (
|
|
@@ -896,3 +900,110 @@ class BaseService:
|
|
|
896
900
|
check_for_errors_standard(self, response_json)
|
|
897
901
|
|
|
898
902
|
return response_json["data"]["usage_record_list"]
|
|
903
|
+
|
|
904
|
+
async def _get_zone_by_device(self, url: str, device: Device) -> Dict[Any, Any]:
|
|
905
|
+
await self._auth_lib.refresh_if_should()
|
|
906
|
+
|
|
907
|
+
payload = olive_create_get_payload_irrigation(device.mac)
|
|
908
|
+
signature = olive_create_signature(payload, self._auth_lib.token.access_token)
|
|
909
|
+
headers = {
|
|
910
|
+
"Accept-Encoding": "gzip",
|
|
911
|
+
"User-Agent": "myapp",
|
|
912
|
+
"appid": OLIVE_APP_ID,
|
|
913
|
+
"appinfo": APP_INFO,
|
|
914
|
+
"phoneid": PHONE_ID,
|
|
915
|
+
"access_token": self._auth_lib.token.access_token,
|
|
916
|
+
"signature2": signature,
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
response_json = await self._auth_lib.get(url, headers=headers, params=payload)
|
|
920
|
+
|
|
921
|
+
check_for_errors_iot(self, response_json)
|
|
922
|
+
|
|
923
|
+
return response_json
|
|
924
|
+
|
|
925
|
+
async def _stop_running_schedule(
|
|
926
|
+
self, url: str, device: Device, action: str
|
|
927
|
+
) -> Dict[Any, Any]:
|
|
928
|
+
await self._auth_lib.refresh_if_should()
|
|
929
|
+
|
|
930
|
+
payload = olive_create_post_payload_irrigation_stop(device.mac, action)
|
|
931
|
+
signature = olive_create_signature(
|
|
932
|
+
json.dumps(payload, separators=(",", ":")),
|
|
933
|
+
self._auth_lib.token.access_token,
|
|
934
|
+
)
|
|
935
|
+
headers = {
|
|
936
|
+
"Accept-Encoding": "gzip",
|
|
937
|
+
"Content-Type": "application/json",
|
|
938
|
+
"User-Agent": "myapp",
|
|
939
|
+
"appid": OLIVE_APP_ID,
|
|
940
|
+
"appinfo": APP_INFO,
|
|
941
|
+
"phoneid": PHONE_ID,
|
|
942
|
+
"access_token": self._auth_lib.token.access_token,
|
|
943
|
+
"signature2": signature,
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
payload_str = json.dumps(payload, separators=(",", ":"))
|
|
947
|
+
response_json = await self._auth_lib.post(
|
|
948
|
+
url, headers=headers, data=payload_str
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
check_for_errors_iot(self, response_json)
|
|
952
|
+
|
|
953
|
+
return response_json
|
|
954
|
+
|
|
955
|
+
async def _start_zone(
|
|
956
|
+
self, url: str, device: Device, zone_number: int, duration: int
|
|
957
|
+
) -> Dict[Any, Any]:
|
|
958
|
+
await self._auth_lib.refresh_if_should()
|
|
959
|
+
|
|
960
|
+
payload = olive_create_post_payload_irrigation_quickrun(
|
|
961
|
+
device.mac, zone_number, duration
|
|
962
|
+
)
|
|
963
|
+
signature = olive_create_signature(
|
|
964
|
+
json.dumps(payload, separators=(",", ":")),
|
|
965
|
+
self._auth_lib.token.access_token,
|
|
966
|
+
)
|
|
967
|
+
headers = {
|
|
968
|
+
"Accept-Encoding": "gzip",
|
|
969
|
+
"Content-Type": "application/json",
|
|
970
|
+
"User-Agent": "myapp",
|
|
971
|
+
"appid": OLIVE_APP_ID,
|
|
972
|
+
"appinfo": APP_INFO,
|
|
973
|
+
"phoneid": PHONE_ID,
|
|
974
|
+
"access_token": self._auth_lib.token.access_token,
|
|
975
|
+
"signature2": signature,
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
payload_str = json.dumps(payload, separators=(",", ":"))
|
|
979
|
+
response_json = await self._auth_lib.post(
|
|
980
|
+
url, headers=headers, data=payload_str
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
check_for_errors_iot(self, response_json)
|
|
984
|
+
|
|
985
|
+
return response_json
|
|
986
|
+
|
|
987
|
+
async def _get_schedule_runs(
|
|
988
|
+
self, url: str, device: Device, limit: int = 2
|
|
989
|
+
) -> Dict[Any, Any]:
|
|
990
|
+
await self._auth_lib.refresh_if_should()
|
|
991
|
+
|
|
992
|
+
payload = olive_create_get_payload_irrigation_schedule_runs(device.mac)
|
|
993
|
+
payload["limit"] = limit
|
|
994
|
+
signature = olive_create_signature(payload, self._auth_lib.token.access_token)
|
|
995
|
+
headers = {
|
|
996
|
+
"Accept-Encoding": "gzip",
|
|
997
|
+
"User-Agent": "myapp",
|
|
998
|
+
"appid": OLIVE_APP_ID,
|
|
999
|
+
"appinfo": APP_INFO,
|
|
1000
|
+
"phoneid": PHONE_ID,
|
|
1001
|
+
"access_token": self._auth_lib.token.access_token,
|
|
1002
|
+
"signature2": signature,
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
response_json = await self._auth_lib.get(url, headers=headers, params=payload)
|
|
1006
|
+
|
|
1007
|
+
check_for_errors_iot(self, response_json)
|
|
1008
|
+
|
|
1009
|
+
return response_json
|
wyzeapy/services/bulb_service.py
CHANGED
|
@@ -15,7 +15,20 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class Bulb(Device):
|
|
18
|
-
"""Bulb class for interacting with Wyze bulbs.
|
|
18
|
+
"""Bulb class for interacting with Wyze bulbs.
|
|
19
|
+
|
|
20
|
+
Note: When created via get_bulbs(), bulb properties (brightness, color,
|
|
21
|
+
color_temp, on) are initialized with default values. Call
|
|
22
|
+
BulbService.update(bulb) to fetch the actual current values from the
|
|
23
|
+
Wyze API.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
bulb_service = await client.bulb_service
|
|
27
|
+
bulbs = await bulb_service.get_bulbs()
|
|
28
|
+
for bulb in bulbs:
|
|
29
|
+
bulb = await bulb_service.update(bulb) # Fetches actual values
|
|
30
|
+
print(f"Brightness: {bulb.brightness}, Color: {bulb.color}")
|
|
31
|
+
"""
|
|
19
32
|
|
|
20
33
|
_brightness: int = 0
|
|
21
34
|
_color_temp: int = 1800
|
|
@@ -90,10 +103,14 @@ class BulbService(BaseService):
|
|
|
90
103
|
"""Bulb service for interacting with Wyze bulbs."""
|
|
91
104
|
|
|
92
105
|
async def update(self, bulb: Bulb) -> Bulb:
|
|
93
|
-
"""
|
|
106
|
+
"""Fetch and update the bulb's current state from the Wyze API.
|
|
107
|
+
|
|
108
|
+
This method retrieves the actual values for brightness, color,
|
|
109
|
+
color_temp, on/off state, and other properties from the Wyze API.
|
|
110
|
+
Must be called after get_bulbs() to get accurate property values.
|
|
94
111
|
|
|
95
112
|
:param bulb: Bulb object to update
|
|
96
|
-
:return: Updated bulb object
|
|
113
|
+
:return: Updated bulb object with current property values
|
|
97
114
|
"""
|
|
98
115
|
# Get updated device_params
|
|
99
116
|
async with BaseService._update_lock:
|
|
@@ -131,7 +148,11 @@ class BulbService(BaseService):
|
|
|
131
148
|
async def get_bulbs(self) -> List[Bulb]:
|
|
132
149
|
"""Get a list of all bulbs.
|
|
133
150
|
|
|
134
|
-
:
|
|
151
|
+
Note: Returned bulbs have default property values (brightness=0,
|
|
152
|
+
color="000000", etc.). Call update(bulb) on each bulb to fetch
|
|
153
|
+
the actual current values from the Wyze API.
|
|
154
|
+
|
|
155
|
+
:return: List of Bulb objects with default property values
|
|
135
156
|
"""
|
|
136
157
|
if self._devices is None:
|
|
137
158
|
self._devices = await self.get_object_list()
|
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
|
|
57
|
+
def __init__(self, dictionary: Dict[Any, Any]):
|
|
58
|
+
self.zone_number: int = dictionary.get("zone_number", 1)
|
|
59
|
+
self.name: str = dictionary.get("name", "Zone 1")
|
|
60
|
+
self.enabled: bool = dictionary.get("enabled", True)
|
|
61
|
+
self.zone_id: str = dictionary.get("zone_id", "zone_id")
|
|
62
|
+
self.smart_duration: int = dictionary.get("smart_duration", 600)
|
|
63
|
+
|
|
64
|
+
# this quickrun duration is used only for running a zone manually
|
|
65
|
+
# the wyze api has no such value, but takes a duration as part of the api call
|
|
66
|
+
# the default value grabs the wyze smart_duration but all further updates
|
|
67
|
+
# are managed through the home assistant state
|
|
68
|
+
self.quickrun_duration: int = dictionary.get("smart_duration", 600)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Irrigation(Device):
|
|
72
|
+
def __init__(self, dictionary: Dict[Any, Any]):
|
|
73
|
+
super().__init__(dictionary)
|
|
74
|
+
|
|
75
|
+
# the below comes from the get_iot_prop call
|
|
76
|
+
self.RSSI: int = 0
|
|
77
|
+
self.IP: str = "192.168.1.100"
|
|
78
|
+
self.sn: str = "SN123456789"
|
|
79
|
+
self.available: bool = False
|
|
80
|
+
self.ssid: str = "ssid"
|
|
81
|
+
# the below comes from the device_info call
|
|
82
|
+
self.zones: List[Zone] = []
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class IrrigationService(BaseService):
|
|
86
|
+
async def update(self, irrigation: Irrigation) -> Irrigation:
|
|
87
|
+
"""Update the irrigation device with latest data from Wyze API."""
|
|
88
|
+
# Get IoT properties
|
|
89
|
+
properties = (await self.get_iot_prop(irrigation))["data"]["props"]
|
|
90
|
+
|
|
91
|
+
# Update device properties
|
|
92
|
+
irrigation.RSSI = properties.get("RSSI", -65)
|
|
93
|
+
irrigation.IP = properties.get("IP", "192.168.1.100")
|
|
94
|
+
irrigation.sn = properties.get("sn", "SN123456789")
|
|
95
|
+
irrigation.ssid = properties.get("ssid", "ssid")
|
|
96
|
+
irrigation.available = (
|
|
97
|
+
properties.get(IrrigationProps.IOT_STATE.value) == "connected"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Get zones
|
|
101
|
+
zones = (await self.get_zone_by_device(irrigation))["data"]["zones"]
|
|
102
|
+
|
|
103
|
+
# Update zones
|
|
104
|
+
irrigation.zones = []
|
|
105
|
+
for zone in zones:
|
|
106
|
+
irrigation.zones.append(Zone(zone))
|
|
107
|
+
|
|
108
|
+
return irrigation
|
|
109
|
+
|
|
110
|
+
async def update_device_props(self, irrigation: Irrigation) -> Irrigation:
|
|
111
|
+
"""Update the irrigation device with latest data from Wyze API."""
|
|
112
|
+
# Get IoT properties
|
|
113
|
+
properties = (await self.get_iot_prop(irrigation))["data"]["props"]
|
|
114
|
+
|
|
115
|
+
# Update device properties
|
|
116
|
+
irrigation.RSSI = properties.get("RSSI")
|
|
117
|
+
irrigation.IP = properties.get("IP")
|
|
118
|
+
irrigation.sn = properties.get("sn")
|
|
119
|
+
irrigation.ssid = properties.get("ssid")
|
|
120
|
+
irrigation.available = (
|
|
121
|
+
properties.get(IrrigationProps.IOT_STATE.value) == "connected"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return irrigation
|
|
125
|
+
|
|
126
|
+
async def get_irrigations(self) -> List[Irrigation]:
|
|
127
|
+
if self._devices is None:
|
|
128
|
+
self._devices = await self.get_object_list()
|
|
129
|
+
|
|
130
|
+
irrigations = [
|
|
131
|
+
device
|
|
132
|
+
for device in self._devices
|
|
133
|
+
if device.type == DeviceTypes.IRRIGATION
|
|
134
|
+
and "BS_WK1" in device.product_model
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
return [Irrigation(irrigation.raw_dict) for irrigation in irrigations]
|
|
138
|
+
|
|
139
|
+
async def start_zone(
|
|
140
|
+
self, irrigation: Device, zone_number: int, quickrun_duration: int
|
|
141
|
+
) -> Dict[Any, Any]:
|
|
142
|
+
"""Start a zone with the specified duration.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
irrigation: The irrigation device
|
|
146
|
+
zone_number: The zone number to start
|
|
147
|
+
quickrun_duration: Duration in seconds to run the zone
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Dict containing the API response
|
|
151
|
+
"""
|
|
152
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/quickrun"
|
|
153
|
+
return await self._start_zone(url, irrigation, zone_number, quickrun_duration)
|
|
154
|
+
|
|
155
|
+
async def stop_running_schedule(self, device: Device) -> Dict[Any, Any]:
|
|
156
|
+
"""Stop any currently running irrigation schedule.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
device: The irrigation device
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Dict containing the API response
|
|
163
|
+
"""
|
|
164
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/runningschedule"
|
|
165
|
+
action = "STOP"
|
|
166
|
+
return await self._stop_running_schedule(url, device, action)
|
|
167
|
+
|
|
168
|
+
async def set_zone_quickrun_duration(
|
|
169
|
+
self, irrigation: Irrigation, zone_number: int, duration: int
|
|
170
|
+
) -> Irrigation:
|
|
171
|
+
"""Set the quickrun duration for a specific zone.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
irrigation: The irrigation device
|
|
175
|
+
zone_number: The zone number to configure
|
|
176
|
+
duration: Duration in seconds for quickrun
|
|
177
|
+
"""
|
|
178
|
+
for zone in irrigation.zones:
|
|
179
|
+
if zone.zone_number == zone_number:
|
|
180
|
+
zone.quickrun_duration = duration
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
return irrigation
|
|
184
|
+
|
|
185
|
+
# Private implementation methods
|
|
186
|
+
async def get_iot_prop(self, device: Device) -> Dict[Any, Any]:
|
|
187
|
+
"""Get IoT properties for a device."""
|
|
188
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/get_iot_prop"
|
|
189
|
+
keys = (
|
|
190
|
+
"zone_state,iot_state,iot_state_update_time,app_version,RSSI,"
|
|
191
|
+
"wifi_mac,sn,device_model,ssid,IP"
|
|
192
|
+
)
|
|
193
|
+
return await self._get_iot_prop(url, device, keys)
|
|
194
|
+
|
|
195
|
+
async def get_device_info(self, device: Device) -> Dict[Any, Any]:
|
|
196
|
+
"""Get device info from Wyze API."""
|
|
197
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/device_info"
|
|
198
|
+
keys = (
|
|
199
|
+
"wiring,sensor,enable_schedules,notification_enable,notification_watering_begins,"
|
|
200
|
+
"notification_watering_ends,notification_watering_is_skipped,skip_low_temp,skip_wind,"
|
|
201
|
+
"skip_rain,skip_saturation"
|
|
202
|
+
)
|
|
203
|
+
return await self._irrigation_device_info(url, device, keys)
|
|
204
|
+
|
|
205
|
+
async def get_zone_by_device(self, device: Device) -> List[Dict[Any, Any]]:
|
|
206
|
+
"""Get zones for a device."""
|
|
207
|
+
url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/zone"
|
|
208
|
+
return await self._get_zone_by_device(url, device)
|
|
209
|
+
|
|
210
|
+
async def get_schedule_runs(self, device: Device) -> Dict[Any, Any]:
|
|
211
|
+
"""Get schedule runs for an irrigation device.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
device: The irrigation device
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Dict containing running status and zone information if running
|
|
218
|
+
"""
|
|
219
|
+
url = (
|
|
220
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/schedule_runs"
|
|
221
|
+
)
|
|
222
|
+
response = await self._get_schedule_runs(url, device, limit=2)
|
|
223
|
+
|
|
224
|
+
# Process the response and return simplified payload
|
|
225
|
+
result = {"running": False}
|
|
226
|
+
|
|
227
|
+
if "data" in response and "schedules" in response["data"]:
|
|
228
|
+
schedules = response["data"]["schedules"]
|
|
229
|
+
for schedule in schedules:
|
|
230
|
+
schedule_state = schedule.get("schedule_state")
|
|
231
|
+
|
|
232
|
+
if schedule_state == "running":
|
|
233
|
+
result["running"] = True
|
|
234
|
+
# Get zone information from zone_runs
|
|
235
|
+
zone_runs = schedule.get("zone_runs")
|
|
236
|
+
# Use the first zone run for zone info
|
|
237
|
+
zone_run = zone_runs[0]
|
|
238
|
+
result["zone_number"] = zone_run.get("zone_number")
|
|
239
|
+
result["zone_name"] = zone_run.get("zone_name")
|
|
240
|
+
break # Found a running schedule, no need to check others
|
|
241
|
+
else:
|
|
242
|
+
_LOGGER.warning(
|
|
243
|
+
"No schedule data found in response for device %s", device.mac
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return result
|
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
3
|
+
from wyzeapy.services.irrigation_service import IrrigationService, Irrigation, Zone
|
|
4
|
+
from wyzeapy.types import DeviceTypes, Device
|
|
5
|
+
from wyzeapy.wyze_auth_lib import WyzeAuthLib
|
|
6
|
+
|
|
7
|
+
# todo: add tests for irrigation service
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestIrrigationService(unittest.IsolatedAsyncioTestCase):
|
|
11
|
+
async def asyncSetUp(self):
|
|
12
|
+
self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
|
|
13
|
+
self.irrigation_service = IrrigationService(auth_lib=self.mock_auth_lib)
|
|
14
|
+
self.irrigation_service.get_iot_prop = AsyncMock()
|
|
15
|
+
self.irrigation_service.get_zone_by_device = AsyncMock()
|
|
16
|
+
self.irrigation_service.get_object_list = AsyncMock()
|
|
17
|
+
self.irrigation_service._get_iot_prop = AsyncMock()
|
|
18
|
+
self.irrigation_service._get_zone_by_device = AsyncMock()
|
|
19
|
+
self.irrigation_service._irrigation_device_info = AsyncMock()
|
|
20
|
+
|
|
21
|
+
# Create test irrigation
|
|
22
|
+
self.test_irrigation = Irrigation(
|
|
23
|
+
{
|
|
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
|
+
|
|
33
|
+
async def test_update_irrigation(self):
|
|
34
|
+
# Mock IoT properties response
|
|
35
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
36
|
+
"data": {
|
|
37
|
+
"props": {
|
|
38
|
+
"RSSI": "-65",
|
|
39
|
+
"IP": "192.168.1.100",
|
|
40
|
+
"sn": "SN123456789",
|
|
41
|
+
"ssid": "TestSSID",
|
|
42
|
+
"iot_state": "connected",
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Mock zones response
|
|
48
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
49
|
+
"data": {
|
|
50
|
+
"zones": [
|
|
51
|
+
{
|
|
52
|
+
"zone_number": 1,
|
|
53
|
+
"name": "Zone 1",
|
|
54
|
+
"enabled": True,
|
|
55
|
+
"zone_id": "zone1",
|
|
56
|
+
"smart_duration": 600,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"zone_number": 2,
|
|
60
|
+
"name": "Zone 2",
|
|
61
|
+
"enabled": True,
|
|
62
|
+
"zone_id": "zone2",
|
|
63
|
+
"smart_duration": 900,
|
|
64
|
+
},
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
70
|
+
|
|
71
|
+
# Test IoT properties
|
|
72
|
+
self.assertEqual(updated_irrigation.RSSI, "-65")
|
|
73
|
+
self.assertEqual(updated_irrigation.IP, "192.168.1.100")
|
|
74
|
+
self.assertEqual(updated_irrigation.sn, "SN123456789")
|
|
75
|
+
self.assertEqual(updated_irrigation.ssid, "TestSSID")
|
|
76
|
+
self.assertTrue(updated_irrigation.available)
|
|
77
|
+
|
|
78
|
+
# Test zones
|
|
79
|
+
self.assertEqual(len(updated_irrigation.zones), 2)
|
|
80
|
+
self.assertEqual(updated_irrigation.zones[0].zone_number, 1)
|
|
81
|
+
self.assertEqual(updated_irrigation.zones[0].name, "Zone 1")
|
|
82
|
+
self.assertTrue(updated_irrigation.zones[0].enabled)
|
|
83
|
+
self.assertEqual(updated_irrigation.zones[0].quickrun_duration, 600)
|
|
84
|
+
self.assertEqual(updated_irrigation.zones[1].zone_number, 2)
|
|
85
|
+
self.assertEqual(updated_irrigation.zones[1].name, "Zone 2")
|
|
86
|
+
self.assertTrue(updated_irrigation.zones[1].enabled)
|
|
87
|
+
self.assertEqual(updated_irrigation.zones[1].quickrun_duration, 900)
|
|
88
|
+
|
|
89
|
+
async def test_get_irrigations(self):
|
|
90
|
+
# Create a mock irrigation device with all required attributes
|
|
91
|
+
mock_irrigation = MagicMock()
|
|
92
|
+
mock_irrigation.type = DeviceTypes.IRRIGATION
|
|
93
|
+
mock_irrigation.product_model = "BS_WK1"
|
|
94
|
+
mock_irrigation.raw_dict = {
|
|
95
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
96
|
+
"product_model": "BS_WK1",
|
|
97
|
+
"mac": "IRRIG123",
|
|
98
|
+
"nickname": "Test Irrigation",
|
|
99
|
+
"device_params": {"ip": "192.168.1.100"},
|
|
100
|
+
"raw_dict": {},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Mock the get_object_list to return our mock irrigation device
|
|
104
|
+
self.irrigation_service.get_object_list.return_value = [mock_irrigation]
|
|
105
|
+
|
|
106
|
+
# Get the irrigations
|
|
107
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
108
|
+
|
|
109
|
+
# Verify the results
|
|
110
|
+
self.assertEqual(len(irrigations), 1)
|
|
111
|
+
self.assertIsInstance(irrigations[0], Irrigation)
|
|
112
|
+
self.assertEqual(irrigations[0].product_model, "BS_WK1")
|
|
113
|
+
self.assertEqual(irrigations[0].mac, "IRRIG123")
|
|
114
|
+
self.irrigation_service.get_object_list.assert_awaited_once()
|
|
115
|
+
|
|
116
|
+
async def test_set_zone_quickrun_duration(self):
|
|
117
|
+
# Setup test irrigation with zones
|
|
118
|
+
self.test_irrigation.zones = [
|
|
119
|
+
Zone(
|
|
120
|
+
{
|
|
121
|
+
"zone_number": 1,
|
|
122
|
+
"name": "Zone 1",
|
|
123
|
+
"enabled": True,
|
|
124
|
+
"zone_id": "zone1",
|
|
125
|
+
"smart_duration": 400,
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
Zone(
|
|
129
|
+
{
|
|
130
|
+
"zone_number": 2,
|
|
131
|
+
"name": "Zone 2",
|
|
132
|
+
"enabled": True,
|
|
133
|
+
"zone_id": "zone2",
|
|
134
|
+
"smart_duration": 900,
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
# Test setting quickrun duration
|
|
140
|
+
await self.irrigation_service.set_zone_quickrun_duration(
|
|
141
|
+
self.test_irrigation, 1, 300
|
|
142
|
+
)
|
|
143
|
+
self.assertEqual(self.test_irrigation.zones[0].quickrun_duration, 300)
|
|
144
|
+
|
|
145
|
+
# Test setting quickrun duration for non-existent zone
|
|
146
|
+
await self.irrigation_service.set_zone_quickrun_duration(
|
|
147
|
+
self.test_irrigation, 999, 300
|
|
148
|
+
)
|
|
149
|
+
# Verify that no zones were modified
|
|
150
|
+
self.assertEqual(len(self.test_irrigation.zones), 2)
|
|
151
|
+
self.assertEqual(
|
|
152
|
+
self.test_irrigation.zones[0].quickrun_duration, 300
|
|
153
|
+
) # First zone changed to 300
|
|
154
|
+
self.assertEqual(
|
|
155
|
+
self.test_irrigation.zones[1].quickrun_duration, 900
|
|
156
|
+
) # Second zone should be unchanged at 900
|
|
157
|
+
|
|
158
|
+
async def test_update_with_invalid_property(self):
|
|
159
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
160
|
+
"data": {"props": {"invalid_property": "some_value", "RSSI": "-65"}}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
164
|
+
"data": {"zones": []}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
168
|
+
self.assertEqual(updated_irrigation.RSSI, "-65")
|
|
169
|
+
# Other properties should maintain their default values
|
|
170
|
+
self.assertEqual(updated_irrigation.IP, "192.168.1.100")
|
|
171
|
+
self.assertEqual(updated_irrigation.sn, "SN123456789")
|
|
172
|
+
self.assertEqual(updated_irrigation.ssid, "ssid")
|
|
173
|
+
|
|
174
|
+
async def test_start_zone(self):
|
|
175
|
+
# Mock the _start_zone method
|
|
176
|
+
self.irrigation_service._start_zone = AsyncMock()
|
|
177
|
+
expected_response = {"data": {"result": "success"}}
|
|
178
|
+
self.irrigation_service._start_zone.return_value = expected_response
|
|
179
|
+
|
|
180
|
+
# Test starting a zone
|
|
181
|
+
result = await self.irrigation_service.start_zone(
|
|
182
|
+
self.test_irrigation, zone_number=1, quickrun_duration=300
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Verify the call was made with correct parameters
|
|
186
|
+
self.irrigation_service._start_zone.assert_awaited_once_with(
|
|
187
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/quickrun",
|
|
188
|
+
self.test_irrigation,
|
|
189
|
+
1,
|
|
190
|
+
300,
|
|
191
|
+
)
|
|
192
|
+
self.assertEqual(result, expected_response)
|
|
193
|
+
|
|
194
|
+
async def test_stop_running_schedule(self):
|
|
195
|
+
# Mock the _stop_running_schedule method
|
|
196
|
+
self.irrigation_service._stop_running_schedule = AsyncMock()
|
|
197
|
+
expected_response = {"data": {"result": "stopped"}}
|
|
198
|
+
self.irrigation_service._stop_running_schedule.return_value = expected_response
|
|
199
|
+
|
|
200
|
+
# Test stopping running schedule
|
|
201
|
+
result = await self.irrigation_service.stop_running_schedule(
|
|
202
|
+
self.test_irrigation
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Verify the call was made with correct parameters
|
|
206
|
+
self.irrigation_service._stop_running_schedule.assert_awaited_once_with(
|
|
207
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/runningschedule",
|
|
208
|
+
self.test_irrigation,
|
|
209
|
+
"STOP",
|
|
210
|
+
)
|
|
211
|
+
self.assertEqual(result, expected_response)
|
|
212
|
+
|
|
213
|
+
async def test_update_device_props(self):
|
|
214
|
+
# Mock IoT properties response
|
|
215
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
216
|
+
"data": {
|
|
217
|
+
"props": {
|
|
218
|
+
"RSSI": "-70",
|
|
219
|
+
"IP": "192.168.1.101",
|
|
220
|
+
"sn": "SN987654321",
|
|
221
|
+
"ssid": "NewSSID",
|
|
222
|
+
"iot_state": "connected",
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
updated_irrigation = await self.irrigation_service.update_device_props(
|
|
228
|
+
self.test_irrigation
|
|
229
|
+
)
|
|
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(
|
|
253
|
+
self.test_irrigation
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Test that device is marked as unavailable
|
|
257
|
+
self.assertFalse(updated_irrigation.available)
|
|
258
|
+
self.assertEqual(updated_irrigation.RSSI, "-80")
|
|
259
|
+
self.assertEqual(updated_irrigation.IP, "192.168.1.102")
|
|
260
|
+
|
|
261
|
+
async def test_get_iot_prop(self):
|
|
262
|
+
# Mock the get_iot_prop method directly to test the public interface
|
|
263
|
+
expected_response = {"data": {"props": {"RSSI": "-65"}}}
|
|
264
|
+
self.irrigation_service.get_iot_prop.return_value = expected_response
|
|
265
|
+
|
|
266
|
+
# Test get_iot_prop
|
|
267
|
+
result = await self.irrigation_service.get_iot_prop(self.test_irrigation)
|
|
268
|
+
|
|
269
|
+
# Verify the call was made and returned expected result
|
|
270
|
+
self.irrigation_service.get_iot_prop.assert_awaited_once_with(
|
|
271
|
+
self.test_irrigation
|
|
272
|
+
)
|
|
273
|
+
self.assertEqual(result, expected_response)
|
|
274
|
+
|
|
275
|
+
async def test_get_device_info(self):
|
|
276
|
+
# Mock the _irrigation_device_info method
|
|
277
|
+
self.irrigation_service._irrigation_device_info = AsyncMock()
|
|
278
|
+
expected_response = {"data": {"props": {"enable_schedules": True}}}
|
|
279
|
+
self.irrigation_service._irrigation_device_info.return_value = expected_response
|
|
280
|
+
|
|
281
|
+
# Test get_device_info
|
|
282
|
+
result = await self.irrigation_service.get_device_info(self.test_irrigation)
|
|
283
|
+
|
|
284
|
+
# Verify the call was made with correct parameters
|
|
285
|
+
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"
|
|
286
|
+
self.irrigation_service._irrigation_device_info.assert_awaited_once_with(
|
|
287
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/device_info",
|
|
288
|
+
self.test_irrigation,
|
|
289
|
+
expected_keys,
|
|
290
|
+
)
|
|
291
|
+
self.assertEqual(result, expected_response)
|
|
292
|
+
|
|
293
|
+
async def test_get_zone_by_device_method(self):
|
|
294
|
+
# Mock the get_zone_by_device method directly to test the public interface
|
|
295
|
+
expected_response = {"data": {"zones": [{"zone_number": 1, "name": "Zone 1"}]}}
|
|
296
|
+
self.irrigation_service.get_zone_by_device.return_value = expected_response
|
|
297
|
+
|
|
298
|
+
# Test get_zone_by_device
|
|
299
|
+
result = await self.irrigation_service.get_zone_by_device(self.test_irrigation)
|
|
300
|
+
|
|
301
|
+
# Verify the call was made and returned expected result
|
|
302
|
+
self.irrigation_service.get_zone_by_device.assert_awaited_once_with(
|
|
303
|
+
self.test_irrigation
|
|
304
|
+
)
|
|
305
|
+
self.assertEqual(result, expected_response)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class TestZone(unittest.TestCase):
|
|
309
|
+
def test_zone_initialization_with_defaults(self):
|
|
310
|
+
# Test zone initialization with minimal data
|
|
311
|
+
zone_data = {"zone_number": 3, "name": "Test Zone"}
|
|
312
|
+
zone = Zone(zone_data)
|
|
313
|
+
|
|
314
|
+
self.assertEqual(zone.zone_number, 3)
|
|
315
|
+
self.assertEqual(zone.name, "Test Zone")
|
|
316
|
+
self.assertTrue(zone.enabled) # Default value
|
|
317
|
+
self.assertEqual(zone.zone_id, "zone_id") # Default value
|
|
318
|
+
self.assertEqual(zone.smart_duration, 600) # Default value
|
|
319
|
+
self.assertEqual(
|
|
320
|
+
zone.quickrun_duration, 600
|
|
321
|
+
) # Default value from smart_duration
|
|
322
|
+
|
|
323
|
+
def test_zone_initialization_with_all_data(self):
|
|
324
|
+
# Test zone initialization with all data
|
|
325
|
+
zone_data = {
|
|
326
|
+
"zone_number": 2,
|
|
327
|
+
"name": "Garden Zone",
|
|
328
|
+
"enabled": False,
|
|
329
|
+
"zone_id": "zone_garden",
|
|
330
|
+
"smart_duration": 1200,
|
|
331
|
+
}
|
|
332
|
+
zone = Zone(zone_data)
|
|
333
|
+
|
|
334
|
+
self.assertEqual(zone.zone_number, 2)
|
|
335
|
+
self.assertEqual(zone.name, "Garden Zone")
|
|
336
|
+
self.assertFalse(zone.enabled)
|
|
337
|
+
self.assertEqual(zone.zone_id, "zone_garden")
|
|
338
|
+
self.assertEqual(zone.smart_duration, 1200)
|
|
339
|
+
self.assertEqual(zone.quickrun_duration, 1200) # Should use smart_duration
|
|
340
|
+
|
|
341
|
+
def test_zone_initialization_empty_dict(self):
|
|
342
|
+
# Test zone initialization with empty dict
|
|
343
|
+
zone = Zone({})
|
|
344
|
+
|
|
345
|
+
self.assertEqual(zone.zone_number, 1) # Default value
|
|
346
|
+
self.assertEqual(zone.name, "Zone 1") # Default value
|
|
347
|
+
self.assertTrue(zone.enabled) # Default value
|
|
348
|
+
self.assertEqual(zone.zone_id, "zone_id") # Default value
|
|
349
|
+
self.assertEqual(zone.smart_duration, 600) # Default value
|
|
350
|
+
self.assertEqual(zone.quickrun_duration, 600) # Default value
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class TestIrrigation(unittest.TestCase):
|
|
354
|
+
def test_irrigation_initialization(self):
|
|
355
|
+
# Test irrigation device initialization
|
|
356
|
+
irrigation_data = {
|
|
357
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
358
|
+
"product_model": "BS_WK1",
|
|
359
|
+
"mac": "IRRIG456",
|
|
360
|
+
"nickname": "Backyard Sprinkler",
|
|
361
|
+
"device_params": {"ip": "192.168.1.200"},
|
|
362
|
+
}
|
|
363
|
+
irrigation = Irrigation(irrigation_data)
|
|
364
|
+
|
|
365
|
+
self.assertEqual(irrigation.product_model, "BS_WK1")
|
|
366
|
+
self.assertEqual(irrigation.mac, "IRRIG456")
|
|
367
|
+
self.assertEqual(irrigation.nickname, "Backyard Sprinkler")
|
|
368
|
+
|
|
369
|
+
# Test default values
|
|
370
|
+
self.assertEqual(irrigation.RSSI, 0)
|
|
371
|
+
self.assertEqual(irrigation.IP, "192.168.1.100")
|
|
372
|
+
self.assertEqual(irrigation.sn, "SN123456789")
|
|
373
|
+
self.assertFalse(irrigation.available)
|
|
374
|
+
self.assertEqual(irrigation.ssid, "ssid")
|
|
375
|
+
self.assertEqual(len(irrigation.zones), 0)
|
|
376
|
+
|
|
377
|
+
def test_irrigation_inheritance(self):
|
|
378
|
+
# Test that Irrigation inherits from Device
|
|
379
|
+
irrigation_data = {
|
|
380
|
+
"product_type": DeviceTypes.IRRIGATION.value,
|
|
381
|
+
"product_model": "BS_WK1",
|
|
382
|
+
"mac": "IRRIG789",
|
|
383
|
+
"nickname": "Front Yard Sprinkler",
|
|
384
|
+
}
|
|
385
|
+
irrigation = Irrigation(irrigation_data)
|
|
386
|
+
|
|
387
|
+
# Test inherited Device properties
|
|
388
|
+
self.assertIsInstance(irrigation, Device)
|
|
389
|
+
self.assertEqual(irrigation.type, DeviceTypes.IRRIGATION)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class TestIrrigationServiceEdgeCases(unittest.IsolatedAsyncioTestCase):
|
|
393
|
+
async def asyncSetUp(self):
|
|
394
|
+
self.mock_auth_lib = MagicMock(spec=WyzeAuthLib)
|
|
395
|
+
self.irrigation_service = IrrigationService(auth_lib=self.mock_auth_lib)
|
|
396
|
+
self.irrigation_service.get_iot_prop = AsyncMock()
|
|
397
|
+
self.irrigation_service.get_zone_by_device = AsyncMock()
|
|
398
|
+
self.irrigation_service.get_object_list = AsyncMock()
|
|
399
|
+
|
|
400
|
+
# Create test irrigation
|
|
401
|
+
self.test_irrigation = Irrigation(
|
|
402
|
+
{
|
|
403
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
404
|
+
"product_model": "BS_WK1",
|
|
405
|
+
"mac": "IRRIG123",
|
|
406
|
+
"nickname": "Test Irrigation",
|
|
407
|
+
"device_params": {"ip": "192.168.1.100"},
|
|
408
|
+
"raw_dict": {},
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
async def test_update_with_empty_zones(self):
|
|
413
|
+
# Mock IoT properties response
|
|
414
|
+
self.irrigation_service.get_iot_prop.return_value = {
|
|
415
|
+
"data": {
|
|
416
|
+
"props": {
|
|
417
|
+
"RSSI": "-65",
|
|
418
|
+
"IP": "192.168.1.100",
|
|
419
|
+
"sn": "SN123456789",
|
|
420
|
+
"ssid": "TestSSID",
|
|
421
|
+
"iot_state": "connected",
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
# Mock empty zones response
|
|
427
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
428
|
+
"data": {"zones": []}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
432
|
+
|
|
433
|
+
# Verify empty zones list
|
|
434
|
+
self.assertEqual(len(updated_irrigation.zones), 0)
|
|
435
|
+
self.assertTrue(updated_irrigation.available)
|
|
436
|
+
|
|
437
|
+
async def test_update_with_missing_iot_props(self):
|
|
438
|
+
# Mock IoT properties response with missing props
|
|
439
|
+
self.irrigation_service.get_iot_prop.return_value = {"data": {"props": {}}}
|
|
440
|
+
|
|
441
|
+
self.irrigation_service.get_zone_by_device.return_value = {
|
|
442
|
+
"data": {"zones": []}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
updated_irrigation = await self.irrigation_service.update(self.test_irrigation)
|
|
446
|
+
|
|
447
|
+
# Verify default values are used
|
|
448
|
+
self.assertEqual(updated_irrigation.RSSI, -65)
|
|
449
|
+
self.assertEqual(updated_irrigation.IP, "192.168.1.100")
|
|
450
|
+
self.assertEqual(updated_irrigation.sn, "SN123456789")
|
|
451
|
+
self.assertEqual(updated_irrigation.ssid, "ssid")
|
|
452
|
+
self.assertFalse(
|
|
453
|
+
updated_irrigation.available
|
|
454
|
+
) # iot_state missing, so not connected
|
|
455
|
+
|
|
456
|
+
async def test_get_irrigations_empty_device_list(self):
|
|
457
|
+
# Mock empty device list
|
|
458
|
+
self.irrigation_service.get_object_list.return_value = []
|
|
459
|
+
|
|
460
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
461
|
+
|
|
462
|
+
# Verify empty list returned
|
|
463
|
+
self.assertEqual(len(irrigations), 0)
|
|
464
|
+
self.irrigation_service.get_object_list.assert_awaited_once()
|
|
465
|
+
|
|
466
|
+
async def test_get_irrigations_no_irrigation_devices(self):
|
|
467
|
+
# Mock device list with non-irrigation devices
|
|
468
|
+
mock_camera = MagicMock()
|
|
469
|
+
mock_camera.type = DeviceTypes.CAMERA
|
|
470
|
+
mock_camera.product_model = "CAM_V1"
|
|
471
|
+
|
|
472
|
+
mock_bulb = MagicMock()
|
|
473
|
+
mock_bulb.type = DeviceTypes.LIGHT
|
|
474
|
+
mock_bulb.product_model = "LIGHT_V1"
|
|
475
|
+
|
|
476
|
+
self.irrigation_service.get_object_list.return_value = [mock_camera, mock_bulb]
|
|
477
|
+
|
|
478
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
479
|
+
|
|
480
|
+
# Verify no irrigation devices returned
|
|
481
|
+
self.assertEqual(len(irrigations), 0)
|
|
482
|
+
|
|
483
|
+
async def test_get_irrigations_wrong_product_model(self):
|
|
484
|
+
# Mock device list with irrigation type but wrong product model
|
|
485
|
+
mock_irrigation = MagicMock()
|
|
486
|
+
mock_irrigation.type = DeviceTypes.IRRIGATION
|
|
487
|
+
mock_irrigation.product_model = "WRONG_MODEL"
|
|
488
|
+
mock_irrigation.raw_dict = {
|
|
489
|
+
"device_type": DeviceTypes.IRRIGATION.value,
|
|
490
|
+
"product_model": "WRONG_MODEL",
|
|
491
|
+
"mac": "IRRIG123",
|
|
492
|
+
"nickname": "Test Irrigation",
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
self.irrigation_service.get_object_list.return_value = [mock_irrigation]
|
|
496
|
+
|
|
497
|
+
irrigations = await self.irrigation_service.get_irrigations()
|
|
498
|
+
|
|
499
|
+
# Verify no irrigation devices returned due to wrong product model
|
|
500
|
+
self.assertEqual(len(irrigations), 0)
|
|
501
|
+
|
|
502
|
+
async def test_set_zone_quickrun_duration_zone_not_found(self):
|
|
503
|
+
# Setup test irrigation with zones
|
|
504
|
+
self.test_irrigation.zones = [
|
|
505
|
+
Zone(
|
|
506
|
+
{
|
|
507
|
+
"zone_number": 1,
|
|
508
|
+
"name": "Zone 1",
|
|
509
|
+
"enabled": True,
|
|
510
|
+
"zone_id": "zone1",
|
|
511
|
+
"smart_duration": 600,
|
|
512
|
+
}
|
|
513
|
+
)
|
|
514
|
+
]
|
|
515
|
+
|
|
516
|
+
# Try to set duration for non-existent zone
|
|
517
|
+
result = await self.irrigation_service.set_zone_quickrun_duration(
|
|
518
|
+
self.test_irrigation,
|
|
519
|
+
99, # Non-existent zone
|
|
520
|
+
300,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Verify existing zone unchanged
|
|
524
|
+
self.assertEqual(self.test_irrigation.zones[0].quickrun_duration, 600)
|
|
525
|
+
self.assertEqual(result, self.test_irrigation)
|
|
526
|
+
|
|
527
|
+
async def test_set_zone_quickrun_duration_no_zones(self):
|
|
528
|
+
# Test with irrigation that has no zones
|
|
529
|
+
self.test_irrigation.zones = []
|
|
530
|
+
|
|
531
|
+
result = await self.irrigation_service.set_zone_quickrun_duration(
|
|
532
|
+
self.test_irrigation, 1, 300
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Verify no error and empty zones list
|
|
536
|
+
self.assertEqual(len(self.test_irrigation.zones), 0)
|
|
537
|
+
self.assertEqual(result, self.test_irrigation)
|
|
538
|
+
|
|
539
|
+
async def test_get_schedule_runs_running_schedule(self):
|
|
540
|
+
# Mock the _get_schedule_runs method
|
|
541
|
+
self.irrigation_service._get_schedule_runs = AsyncMock()
|
|
542
|
+
mock_response = {
|
|
543
|
+
"data": {
|
|
544
|
+
"schedules": [
|
|
545
|
+
{
|
|
546
|
+
"schedule_state": "running",
|
|
547
|
+
"schedule_name": "Morning Watering",
|
|
548
|
+
"zone_runs": [
|
|
549
|
+
{
|
|
550
|
+
"zone_number": 3,
|
|
551
|
+
"zone_name": "Backyard S",
|
|
552
|
+
"start_ts": 1746376809,
|
|
553
|
+
"end_ts": 1746376869,
|
|
554
|
+
}
|
|
555
|
+
],
|
|
556
|
+
}
|
|
557
|
+
]
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
self.irrigation_service._get_schedule_runs.return_value = mock_response
|
|
561
|
+
|
|
562
|
+
# Test get_schedule_runs with running schedule
|
|
563
|
+
result = await self.irrigation_service.get_schedule_runs(self.test_irrigation)
|
|
564
|
+
|
|
565
|
+
# Verify the call was made with correct parameters
|
|
566
|
+
self.irrigation_service._get_schedule_runs.assert_awaited_once_with(
|
|
567
|
+
"https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/schedule_runs",
|
|
568
|
+
self.test_irrigation,
|
|
569
|
+
limit=2,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# Verify the result
|
|
573
|
+
expected_result = {"running": True, "zone_number": 3, "zone_name": "Backyard S"}
|
|
574
|
+
self.assertEqual(result, expected_result)
|
|
575
|
+
|
|
576
|
+
async def test_get_schedule_runs_past_schedule(self):
|
|
577
|
+
# Mock the _get_schedule_runs method
|
|
578
|
+
self.irrigation_service._get_schedule_runs = AsyncMock()
|
|
579
|
+
mock_response = {
|
|
580
|
+
"data": {
|
|
581
|
+
"schedules": [
|
|
582
|
+
{
|
|
583
|
+
"schedule_state": "past",
|
|
584
|
+
"schedule_name": "Evening Watering",
|
|
585
|
+
"zone_runs": [
|
|
586
|
+
{
|
|
587
|
+
"zone_number": 1,
|
|
588
|
+
"zone_name": "Front Yard",
|
|
589
|
+
"start_ts": 1746376809,
|
|
590
|
+
"end_ts": 1746376869,
|
|
591
|
+
}
|
|
592
|
+
],
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
self.irrigation_service._get_schedule_runs.return_value = mock_response
|
|
598
|
+
|
|
599
|
+
# Test get_schedule_runs with past schedule
|
|
600
|
+
result = await self.irrigation_service.get_schedule_runs(self.test_irrigation)
|
|
601
|
+
|
|
602
|
+
# Verify the result
|
|
603
|
+
expected_result = {"running": False}
|
|
604
|
+
self.assertEqual(result, expected_result)
|
|
605
|
+
|
|
606
|
+
async def test_get_schedule_runs_no_schedules(self):
|
|
607
|
+
# Mock the _get_schedule_runs method
|
|
608
|
+
self.irrigation_service._get_schedule_runs = AsyncMock()
|
|
609
|
+
mock_response = {"data": {"schedules": []}}
|
|
610
|
+
self.irrigation_service._get_schedule_runs.return_value = mock_response
|
|
611
|
+
|
|
612
|
+
# Test get_schedule_runs with no schedules
|
|
613
|
+
result = await self.irrigation_service.get_schedule_runs(self.test_irrigation)
|
|
614
|
+
|
|
615
|
+
# Verify the result
|
|
616
|
+
expected_result = {"running": False}
|
|
617
|
+
self.assertEqual(result, expected_result)
|
|
618
|
+
|
|
619
|
+
async def test_get_schedule_runs_no_data(self):
|
|
620
|
+
# Mock the _get_schedule_runs method
|
|
621
|
+
self.irrigation_service._get_schedule_runs = AsyncMock()
|
|
622
|
+
mock_response = {} # No data field
|
|
623
|
+
self.irrigation_service._get_schedule_runs.return_value = mock_response
|
|
624
|
+
|
|
625
|
+
# Test get_schedule_runs with no data
|
|
626
|
+
with self.assertLogs(
|
|
627
|
+
"wyzeapy.services.irrigation_service", level="WARNING"
|
|
628
|
+
) as log:
|
|
629
|
+
result = await self.irrigation_service.get_schedule_runs(
|
|
630
|
+
self.test_irrigation
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Verify the result
|
|
634
|
+
expected_result = {"running": False}
|
|
635
|
+
self.assertEqual(result, expected_result)
|
|
636
|
+
|
|
637
|
+
# Verify warning was logged
|
|
638
|
+
self.assertIn(
|
|
639
|
+
"No schedule data found in response for device IRRIG123", log.output[0]
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
async def test_get_schedule_runs_multiple_schedules_running_first(self):
|
|
643
|
+
# Mock the _get_schedule_runs method
|
|
644
|
+
self.irrigation_service._get_schedule_runs = AsyncMock()
|
|
645
|
+
mock_response = {
|
|
646
|
+
"data": {
|
|
647
|
+
"schedules": [
|
|
648
|
+
{
|
|
649
|
+
"schedule_state": "running",
|
|
650
|
+
"schedule_name": "Morning Watering",
|
|
651
|
+
"zone_runs": [
|
|
652
|
+
{
|
|
653
|
+
"zone_number": 2,
|
|
654
|
+
"zone_name": "Side Yard",
|
|
655
|
+
"start_ts": 1746376809,
|
|
656
|
+
"end_ts": 1746376869,
|
|
657
|
+
}
|
|
658
|
+
],
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
"schedule_state": "past",
|
|
662
|
+
"schedule_name": "Evening Watering",
|
|
663
|
+
"zone_runs": [
|
|
664
|
+
{
|
|
665
|
+
"zone_number": 1,
|
|
666
|
+
"zone_name": "Front Yard",
|
|
667
|
+
"start_ts": 1746376809,
|
|
668
|
+
"end_ts": 1746376869,
|
|
669
|
+
}
|
|
670
|
+
],
|
|
671
|
+
},
|
|
672
|
+
]
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
self.irrigation_service._get_schedule_runs.return_value = mock_response
|
|
676
|
+
|
|
677
|
+
# Test get_schedule_runs with multiple schedules, first one running
|
|
678
|
+
result = await self.irrigation_service.get_schedule_runs(self.test_irrigation)
|
|
679
|
+
|
|
680
|
+
# Verify the result uses the first running schedule
|
|
681
|
+
expected_result = {"running": True, "zone_number": 2, "zone_name": "Side Yard"}
|
|
682
|
+
self.assertEqual(result, expected_result)
|
|
683
|
+
|
|
684
|
+
async def test_get_schedule_runs_multiple_schedules_running_second(self):
|
|
685
|
+
# Mock the _get_schedule_runs method
|
|
686
|
+
self.irrigation_service._get_schedule_runs = AsyncMock()
|
|
687
|
+
mock_response = {
|
|
688
|
+
"data": {
|
|
689
|
+
"schedules": [
|
|
690
|
+
{
|
|
691
|
+
"schedule_state": "past",
|
|
692
|
+
"schedule_name": "Evening Watering",
|
|
693
|
+
"zone_runs": [
|
|
694
|
+
{
|
|
695
|
+
"zone_number": 1,
|
|
696
|
+
"zone_name": "Front Yard",
|
|
697
|
+
"start_ts": 1746376809,
|
|
698
|
+
"end_ts": 1746376869,
|
|
699
|
+
}
|
|
700
|
+
],
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
"schedule_state": "running",
|
|
704
|
+
"schedule_name": "Morning Watering",
|
|
705
|
+
"zone_runs": [
|
|
706
|
+
{
|
|
707
|
+
"zone_number": 4,
|
|
708
|
+
"zone_name": "Garden Area",
|
|
709
|
+
"start_ts": 1746376809,
|
|
710
|
+
"end_ts": 1746376869,
|
|
711
|
+
}
|
|
712
|
+
],
|
|
713
|
+
},
|
|
714
|
+
]
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
self.irrigation_service._get_schedule_runs.return_value = mock_response
|
|
718
|
+
|
|
719
|
+
# Test get_schedule_runs with multiple schedules, second one running
|
|
720
|
+
result = await self.irrigation_service.get_schedule_runs(self.test_irrigation)
|
|
721
|
+
|
|
722
|
+
# Verify the result uses the first running schedule found
|
|
723
|
+
expected_result = {
|
|
724
|
+
"running": True,
|
|
725
|
+
"zone_number": 4,
|
|
726
|
+
"zone_name": "Garden Area",
|
|
727
|
+
}
|
|
728
|
+
self.assertEqual(result, expected_result)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
if __name__ == "__main__":
|
|
732
|
+
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"
|
wyzeapy/utils.py
CHANGED
|
@@ -81,8 +81,13 @@ def wyze_decrypt_cbc(key: str, enc_hex_str: str) -> str:
|
|
|
81
81
|
|
|
82
82
|
Returns:
|
|
83
83
|
The decrypted plaintext string.
|
|
84
|
+
|
|
85
|
+
Note:
|
|
86
|
+
MD5 is used here because it is required by Wyze's proprietary API protocol.
|
|
87
|
+
This is not a security vulnerability - it's mandatory for API compatibility.
|
|
84
88
|
"""
|
|
85
|
-
|
|
89
|
+
# MD5 is required by Wyze's API protocol - not a security choice but API compatibility
|
|
90
|
+
key_hash = hashlib.md5(key.encode("utf-8")).digest() # nosec B324
|
|
86
91
|
|
|
87
92
|
iv = b"0123456789ABCDEF"
|
|
88
93
|
cipher = AES.new(key_hash, AES.MODE_CBC, iv)
|
|
@@ -104,10 +109,15 @@ def create_password(password: str) -> str:
|
|
|
104
109
|
|
|
105
110
|
Returns:
|
|
106
111
|
The hashed password as a hex string.
|
|
112
|
+
|
|
113
|
+
Note:
|
|
114
|
+
Triple MD5 is mandated by Wyze's authentication API. This cannot be changed
|
|
115
|
+
without breaking compatibility with Wyze's servers.
|
|
107
116
|
"""
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
# MD5 is required by Wyze's authentication protocol - not a security choice
|
|
118
|
+
hex1 = hashlib.md5(password.encode()).hexdigest() # nosec B324
|
|
119
|
+
hex2 = hashlib.md5(hex1.encode()).hexdigest() # nosec B324
|
|
120
|
+
return hashlib.md5(hex2.encode()).hexdigest() # nosec B324
|
|
111
121
|
|
|
112
122
|
|
|
113
123
|
def check_for_errors_standard(service, response_json: Dict[str, Any]) -> None:
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wyzeapy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.31
|
|
4
4
|
Summary: A library for interacting with Wyze devices
|
|
5
5
|
Author-email: Katie Mulliken <katie@mulliken.net>
|
|
6
6
|
License: GPL-3.0-only
|
|
7
7
|
Requires-Python: >=3.11.0
|
|
8
|
-
Requires-Dist: aiodns<
|
|
8
|
+
Requires-Dist: aiodns<5.0.0,>=3.2.0
|
|
9
9
|
Requires-Dist: aiohttp<4.0.0,>=3.11.12
|
|
10
10
|
Requires-Dist: pycryptodome<4.0.0,>=3.21.0
|
|
11
11
|
Provides-Extra: dev
|
|
12
|
-
Requires-Dist: pdoc<
|
|
13
|
-
Requires-Dist: pytest<
|
|
12
|
+
Requires-Dist: pdoc<17.0.0,>=15.0.3; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest<10.0.0,>=7.0.0; extra == 'dev'
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
wyzeapy/__init__.py,sha256=
|
|
1
|
+
wyzeapy/__init__.py,sha256=4peDFw5zdEc9T6iR2NxZ1mwhcG3ShW91gceBJqcWZy8,17244
|
|
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=
|
|
7
|
-
wyzeapy/utils.py,sha256=
|
|
5
|
+
wyzeapy/payload_factory.py,sha256=1QUEgqSSDwEjKv1ew-jQYoXcFk0uf5B4gODw83ep2zg,20638
|
|
6
|
+
wyzeapy/types.py,sha256=LPLc86FkEy8rUoPMwG8xB73NCTVRkJ1O5w7uSfDydLQ,6785
|
|
7
|
+
wyzeapy/utils.py,sha256=_aoP9H7RXuND3nCMP3gJRLBxX6Dsxag81RLm83G5Pu4,7969
|
|
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=
|
|
11
|
-
wyzeapy/services/bulb_service.py,sha256=
|
|
10
|
+
wyzeapy/services/base_service.py,sha256=3FsbZHm6ndc6pSGCwrwh6SRgGb7HDK_L5lzuSjaLgO0,35547
|
|
11
|
+
wyzeapy/services/bulb_service.py,sha256=4Z_rX40OGqzRuKz_fgyWGb4di7jtyJSB_ZvFFCDlcCo,8821
|
|
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=6HzwFRTjZcM4-Y9exSn5EmS2DZbl1gF_uaiMpK2Kd6c,8670
|
|
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=ui3ZlqLUkoLb-uebK8Q1A83d9lAV9WIuHGbIrqPLyqg,28926
|
|
22
|
+
wyzeapy-0.5.31.dist-info/METADATA,sha256=ps5g5kCz_rY2XLNeVS31nSuX4ZXp1xVQmNJTrCLsTOw,446
|
|
23
|
+
wyzeapy-0.5.31.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
24
|
+
wyzeapy-0.5.31.dist-info/RECORD,,
|