wyzeapy 0.5.30__py3-none-any.whl → 0.5.31rc1__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 CHANGED
@@ -443,7 +443,7 @@ class Wyzeapy:
443
443
  if self._sensor_service is None:
444
444
  self._sensor_service = SensorService(self._auth_lib)
445
445
  return self._sensor_service
446
-
446
+
447
447
  @property
448
448
  async def irrigation_service(self) -> IrrigationService:
449
449
  """Returns an instance of the irrigation service"""
@@ -51,37 +51,41 @@ 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
+
54
55
  def olive_create_get_payload_irrigation(device_mac: str) -> Dict[str, Any]:
55
56
  nonce = int(time.time() * 1000)
56
57
 
57
- return {
58
- 'device_id': device_mac,
59
- 'nonce': str(nonce)
60
- }
58
+ return {"device_id": device_mac, "nonce": str(nonce)}
61
59
 
62
- def olive_create_post_payload_irrigation_stop(device_mac: str, action: str) -> Dict[str, Any]:
60
+
61
+ def olive_create_post_payload_irrigation_stop(
62
+ device_mac: str, action: str
63
+ ) -> Dict[str, Any]:
63
64
  nonce = int(time.time() * 1000)
64
65
 
65
- return {
66
- 'device_id': device_mac,
67
- 'nonce': str(nonce),
68
- "action": action
69
- }
66
+ return {"device_id": device_mac, "nonce": str(nonce), "action": action}
70
67
 
71
- def olive_create_post_payload_irrigation_quickrun(device_mac: str, zone_number: int, duration: int) -> Dict[str, Any]:
68
+
69
+ def olive_create_post_payload_irrigation_quickrun(
70
+ device_mac: str, zone_number: int, duration: int
71
+ ) -> Dict[str, Any]:
72
72
  nonce = int(time.time() * 1000)
73
73
 
74
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
- ]
75
+ "device_id": device_mac,
76
+ "nonce": str(nonce),
77
+ "zone_runs": [{"zone_number": zone_number, "duration": duration}],
83
78
  }
84
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
+
85
89
  def olive_create_post_payload(
86
90
  device_mac: str, device_model: str, prop_key: str, value: Any
87
91
  ) -> Dict[str, Any]:
@@ -39,6 +39,7 @@ from ..payload_factory import (
39
39
  olive_create_get_payload_irrigation,
40
40
  olive_create_post_payload_irrigation_stop,
41
41
  olive_create_post_payload_irrigation_quickrun,
42
+ olive_create_get_payload_irrigation_schedule_runs,
42
43
  )
43
44
  from ..types import PropertyIDs, Device, DeviceMgmtToggleType
44
45
  from ..utils import (
@@ -906,13 +907,13 @@ class BaseService:
906
907
  payload = olive_create_get_payload_irrigation(device.mac)
907
908
  signature = olive_create_signature(payload, self._auth_lib.token.access_token)
908
909
  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
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,
916
917
  }
917
918
 
918
919
  response_json = await self._auth_lib.get(url, headers=headers, params=payload)
@@ -921,50 +922,87 @@ class BaseService:
921
922
 
922
923
  return response_json
923
924
 
924
-
925
- async def _stop_running_schedule(self, url: str, device: Device, action: str) -> Dict[Any, Any]:
925
+ async def _stop_running_schedule(
926
+ self, url: str, device: Device, action: str
927
+ ) -> Dict[Any, Any]:
926
928
  await self._auth_lib.refresh_if_should()
927
929
 
928
930
  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
+ signature = olive_create_signature(
932
+ json.dumps(payload, separators=(",", ":")),
933
+ self._auth_lib.token.access_token,
934
+ )
931
935
  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
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,
940
944
  }
941
945
 
942
- payload_str = json.dumps(payload, separators=(',', ':'))
943
- response_json = await self._auth_lib.post(url, headers=headers, data=payload_str)
946
+ payload_str = json.dumps(payload, separators=(",", ":"))
947
+ response_json = await self._auth_lib.post(
948
+ url, headers=headers, data=payload_str
949
+ )
944
950
 
945
951
  check_for_errors_iot(self, response_json)
946
952
 
947
953
  return response_json
948
954
 
949
- async def _start_zone(self, url: str, device: Device, zone_number: int, duration: int) -> Dict[Any, Any]:
955
+ async def _start_zone(
956
+ self, url: str, device: Device, zone_number: int, duration: int
957
+ ) -> Dict[Any, Any]:
950
958
  await self._auth_lib.refresh_if_should()
951
959
 
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)
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
+ )
955
967
  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
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,
964
976
  }
965
977
 
966
- payload_str = json.dumps(payload, separators=(',', ':'))
967
- response_json = await self._auth_lib.post(url, headers=headers, data=payload_str)
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)
968
1006
 
969
1007
  check_for_errors_iot(self, response_json)
970
1008
 
@@ -88,8 +88,10 @@ class CameraService(BaseService):
88
88
  if property is PropertyIDs.CAMERA_SIREN:
89
89
  camera.siren = value == "1"
90
90
  if property is PropertyIDs.ACCESSORY:
91
+ # Bulb Cam (HL_BC): '1' = ON, '2' = OFF
92
+ # Other cameras with accessories: same logic
91
93
  camera.floodlight = value == "1"
92
- if camera.device_params["dongle_product_model"] == "HL_CGDC":
94
+ if camera.device_params.get("dongle_product_model") == "HL_CGDC":
93
95
  camera.garage = (
94
96
  value == "1"
95
97
  ) # 1 = open, 2 = closed by automation or smart platform (Alexa, Google Home, Rules), 0 = closed by app
@@ -186,7 +188,7 @@ class CameraService(BaseService):
186
188
  else:
187
189
  await self._run_action(camera, "siren_off")
188
190
 
189
- # Also controls lamp socket and BCP spotlight
191
+ # Also controls lamp socket, BCP spotlight, and Bulb Cam light
190
192
  async def floodlight_on(self, camera: Camera):
191
193
  if camera.product_model == "AN_RSCW":
192
194
  await self._run_action_devicemgmt(
@@ -196,10 +198,13 @@ class CameraService(BaseService):
196
198
  await self._run_action_devicemgmt(
197
199
  camera, "floodlight", "1"
198
200
  ) # Some camera models use a diffrent api
201
+ elif camera.product_model == "HL_BC":
202
+ # Bulb Cam uses run_action with floodlight_on action
203
+ await self._run_action(camera, "floodlight_on")
199
204
  else:
200
205
  await self._set_property(camera, PropertyIDs.ACCESSORY.value, "1")
201
206
 
202
- # Also controls lamp socket and BCP spotlight
207
+ # Also controls lamp socket, BCP spotlight, and Bulb Cam light
203
208
  async def floodlight_off(self, camera: Camera):
204
209
  if camera.product_model == "AN_RSCW":
205
210
  await self._run_action_devicemgmt(
@@ -209,6 +214,9 @@ class CameraService(BaseService):
209
214
  await self._run_action_devicemgmt(
210
215
  camera, "floodlight", "0"
211
216
  ) # Some camera models use a diffrent api
217
+ elif camera.product_model == "HL_BC":
218
+ # Bulb Cam uses run_action with floodlight_off action
219
+ await self._run_action(camera, "floodlight_off")
212
220
  else:
213
221
  await self._set_property(camera, PropertyIDs.ACCESSORY.value, "2")
214
222
 
@@ -42,29 +42,31 @@ class SlopeType(Enum):
42
42
 
43
43
 
44
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'
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
52
 
53
53
 
54
54
  class Zone:
55
55
  """Represents a single irrigation zone."""
56
+
56
57
  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
-
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
+
63
64
  # this quickrun duration is used only for running a zone manually
64
65
  # the wyze api has no such value, but takes a duration as part of the api call
65
66
  # the default value grabs the wyze smart_duration but all further updates
66
67
  # are managed through the home assistant state
67
- self.quickrun_duration: int = dictionary.get('smart_duration', 600)
68
+ self.quickrun_duration: int = dictionary.get("smart_duration", 600)
69
+
68
70
 
69
71
  class Irrigation(Device):
70
72
  def __init__(self, dictionary: Dict[Any, Any]):
@@ -84,36 +86,40 @@ class IrrigationService(BaseService):
84
86
  async def update(self, irrigation: Irrigation) -> Irrigation:
85
87
  """Update the irrigation device with latest data from Wyze API."""
86
88
  # Get IoT properties
87
- properties = (await self.get_iot_prop(irrigation))['data']['props']
88
-
89
+ properties = (await self.get_iot_prop(irrigation))["data"]["props"]
90
+
89
91
  # 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")
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
+ )
95
99
 
96
100
  # Get zones
97
- zones = (await self.get_zone_by_device(irrigation))['data']['zones']
98
-
101
+ zones = (await self.get_zone_by_device(irrigation))["data"]["zones"]
102
+
99
103
  # Update zones
100
104
  irrigation.zones = []
101
105
  for zone in zones:
102
106
  irrigation.zones.append(Zone(zone))
103
-
107
+
104
108
  return irrigation
105
-
109
+
106
110
  async def update_device_props(self, irrigation: Irrigation) -> Irrigation:
107
111
  """Update the irrigation device with latest data from Wyze API."""
108
112
  # Get IoT properties
109
- properties = (await self.get_iot_prop(irrigation))['data']['props']
110
-
113
+ properties = (await self.get_iot_prop(irrigation))["data"]["props"]
114
+
111
115
  # 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')
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
+ )
117
123
 
118
124
  return irrigation
119
125
 
@@ -121,18 +127,25 @@ class IrrigationService(BaseService):
121
127
  if self._devices is None:
122
128
  self._devices = await self.get_object_list()
123
129
 
124
- irrigations = [device for device in self._devices if device.type == DeviceTypes.IRRIGATION and "BS_WK1" in device.product_model]
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
+ ]
125
136
 
126
137
  return [Irrigation(irrigation.raw_dict) for irrigation in irrigations]
127
138
 
128
- async def start_zone(self, irrigation: Device, zone_number: int, quickrun_duration: int) -> Dict[Any, Any]:
139
+ async def start_zone(
140
+ self, irrigation: Device, zone_number: int, quickrun_duration: int
141
+ ) -> Dict[Any, Any]:
129
142
  """Start a zone with the specified duration.
130
-
143
+
131
144
  Args:
132
145
  irrigation: The irrigation device
133
146
  zone_number: The zone number to start
134
147
  quickrun_duration: Duration in seconds to run the zone
135
-
148
+
136
149
  Returns:
137
150
  Dict containing the API response
138
151
  """
@@ -141,20 +154,22 @@ class IrrigationService(BaseService):
141
154
 
142
155
  async def stop_running_schedule(self, device: Device) -> Dict[Any, Any]:
143
156
  """Stop any currently running irrigation schedule.
144
-
157
+
145
158
  Args:
146
159
  device: The irrigation device
147
-
160
+
148
161
  Returns:
149
162
  Dict containing the API response
150
163
  """
151
164
  url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/runningschedule"
152
- action = "STOP"
165
+ action = "STOP"
153
166
  return await self._stop_running_schedule(url, device, action)
154
167
 
155
- async def set_zone_quickrun_duration(self, irrigation: Irrigation, zone_number: int, duration: int) -> Irrigation:
168
+ async def set_zone_quickrun_duration(
169
+ self, irrigation: Irrigation, zone_number: int, duration: int
170
+ ) -> Irrigation:
156
171
  """Set the quickrun duration for a specific zone.
157
-
172
+
158
173
  Args:
159
174
  irrigation: The irrigation device
160
175
  zone_number: The zone number to configure
@@ -164,26 +179,68 @@ class IrrigationService(BaseService):
164
179
  if zone.zone_number == zone_number:
165
180
  zone.quickrun_duration = duration
166
181
  break
167
-
182
+
168
183
  return irrigation
169
184
 
170
185
  # Private implementation methods
171
186
  async def get_iot_prop(self, device: Device) -> Dict[Any, Any]:
172
187
  """Get IoT properties for a device."""
173
188
  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'
189
+ keys = (
190
+ "zone_state,iot_state,iot_state_update_time,app_version,RSSI,"
191
+ "wifi_mac,sn,device_model,ssid,IP"
192
+ )
176
193
  return await self._get_iot_prop(url, device, keys)
177
194
 
178
195
  async def get_device_info(self, device: Device) -> Dict[Any, Any]:
179
196
  """Get device info from Wyze API."""
180
197
  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'
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
+ )
184
203
  return await self._irrigation_device_info(url, device, keys)
185
204
 
186
205
  async def get_zone_by_device(self, device: Device) -> List[Dict[Any, Any]]:
187
206
  """Get zones for a device."""
188
207
  url = "https://wyze-lockwood-service.wyzecam.com/plugin/irrigation/zone"
189
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