pypetkitapi 1.2.0__tar.gz → 1.11.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Jezza34000
3
+ Copyright (c) 2024 - 2025 Jezza34000
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,3 +1,24 @@
1
+ Metadata-Version: 2.3
2
+ Name: pypetkitapi
3
+ Version: 1.11.2
4
+ Summary: Python client for PetKit API
5
+ License: MIT
6
+ Author: Jezza34000
7
+ Author-email: info@mail.com
8
+ Requires-Python: >=3.11
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
15
+ Requires-Dist: aiohttp (>=3.10.10,<4.0.0)
16
+ Requires-Dist: m3u8 (>=6.0)
17
+ Requires-Dist: pycryptodome (>=3.19.1,<4.0.0)
18
+ Requires-Dist: pydantic (>=1.10.18,<3.0.0)
19
+ Project-URL: Homepage, https://github.com/Jezza34000/pypetkit
20
+ Description-Content-Type: text/markdown
21
+
1
22
  # Petkit API Client
2
23
 
3
24
  ---
@@ -12,6 +33,12 @@
12
33
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
13
34
  [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
14
35
  [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
36
+ [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=bugs)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
37
+ [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
38
+ [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
39
+ [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
40
+
41
+ [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
15
42
 
16
43
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit]
17
44
  [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black]
@@ -46,9 +73,11 @@ pip install pypetkitapi
46
73
  ## Usage Example:
47
74
 
48
75
  ```python
76
+ import asyncio
77
+ import logging
49
78
  import aiohttp
50
79
  from pypetkitapi.client import PetKitClient
51
- from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
80
+ from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, DeviceAction, LitterCommand
52
81
 
53
82
  logging.basicConfig(level=logging.DEBUG)
54
83
 
@@ -57,37 +86,50 @@ async def main():
57
86
  client = PetKitClient(
58
87
  username="username", # Your PetKit account username or id
59
88
  password="password", # Your PetKit account password
60
- region="FR", # Your region or country code (e.g. FR, US, etc.)
61
- timezone="Europe/Paris", # Your timezone
89
+ region="FR", # Your region or country code (e.g. FR, US,CN etc.)
90
+ timezone="Europe/Paris", # Your timezone(e.g. "Asia/Shanghai")
62
91
  session=session,
63
92
  )
64
93
 
65
94
  await client.get_devices_data()
66
95
 
67
- # Read the account data
68
- print(client.account_data)
96
+ # Lists all devices and pet from account
97
+
98
+ for key, value in client.petkit_entities.items():
99
+ print(f"{key}: {type(value).__name__} - {value.name}")
69
100
 
70
- # Read the devices data
71
- print(client.petkit_entities)
101
+ # Select a device
102
+ device_id = key
103
+ # Read devices or pet information
104
+ print(client.petkit_entities[device_id])
72
105
 
73
106
  # Send command to the devices
74
107
  ### Example 1 : Turn on the indicator light
75
108
  ### Device_ID, Command, Payload
76
- await client.send_api_request(123456789, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
109
+ await client.send_api_request(device_id, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
77
110
 
78
111
  ### Example 2 : Feed the pet
79
112
  ### Device_ID, Command, Payload
80
- await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
113
+ # simple hopper :
114
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount": 1})
115
+ # dual hopper :
116
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount1": 2})
117
+ # or
118
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount2": 2})
81
119
 
82
120
  ### Example 3 : Start the cleaning process
83
121
  ### Device_ID, Command, Payload
84
- await client.send_api_request(123456789, LitterCommand.CONTROL_DEVICE, {LBAction.START: LBCommand.CLEANING})
122
+ await client.send_api_request(device_id, LitterCommand.CONTROL_DEVICE, {DeviceAction.START: LBCommand.CLEANING})
85
123
 
86
124
 
87
125
  if __name__ == "__main__":
88
126
  asyncio.run(main())
89
127
  ```
90
128
 
129
+ ## More example usage
130
+
131
+ Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
132
+
91
133
  ## Contributing
92
134
 
93
135
  Contributions are welcome! Please open an issue or submit a pull request.
@@ -95,3 +137,4 @@ Contributions are welcome! Please open an issue or submit a pull request.
95
137
  ## License
96
138
 
97
139
  This project is licensed under the MIT License. See the LICENSE file for details.
140
+
@@ -1,20 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: pypetkitapi
3
- Version: 1.2.0
4
- Summary: Python client for PetKit API
5
- Home-page: https://github.com/Jezza34000/pypetkit
6
- License: MIT
7
- Author: Jezza34000
8
- Author-email: info@mail.com
9
- Requires-Python: >=3.11
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.11
13
- Classifier: Programming Language :: Python :: 3.12
14
- Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: aiohttp (>=3.11.0,<4.0.0)
16
- Description-Content-Type: text/markdown
17
-
18
1
  # Petkit API Client
19
2
 
20
3
  ---
@@ -29,6 +12,12 @@ Description-Content-Type: text/markdown
29
12
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
30
13
  [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
31
14
  [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
15
+ [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=bugs)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
16
+ [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
17
+ [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
18
+ [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
19
+
20
+ [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=Jezza34000_py-petkit-api&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
32
21
 
33
22
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit]
34
23
  [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black]
@@ -63,9 +52,11 @@ pip install pypetkitapi
63
52
  ## Usage Example:
64
53
 
65
54
  ```python
55
+ import asyncio
56
+ import logging
66
57
  import aiohttp
67
58
  from pypetkitapi.client import PetKitClient
68
- from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
59
+ from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, DeviceAction, LitterCommand
69
60
 
70
61
  logging.basicConfig(level=logging.DEBUG)
71
62
 
@@ -74,37 +65,50 @@ async def main():
74
65
  client = PetKitClient(
75
66
  username="username", # Your PetKit account username or id
76
67
  password="password", # Your PetKit account password
77
- region="FR", # Your region or country code (e.g. FR, US, etc.)
78
- timezone="Europe/Paris", # Your timezone
68
+ region="FR", # Your region or country code (e.g. FR, US,CN etc.)
69
+ timezone="Europe/Paris", # Your timezone(e.g. "Asia/Shanghai")
79
70
  session=session,
80
71
  )
81
72
 
82
73
  await client.get_devices_data()
83
74
 
84
- # Read the account data
85
- print(client.account_data)
75
+ # Lists all devices and pet from account
76
+
77
+ for key, value in client.petkit_entities.items():
78
+ print(f"{key}: {type(value).__name__} - {value.name}")
86
79
 
87
- # Read the devices data
88
- print(client.petkit_entities)
80
+ # Select a device
81
+ device_id = key
82
+ # Read devices or pet information
83
+ print(client.petkit_entities[device_id])
89
84
 
90
85
  # Send command to the devices
91
86
  ### Example 1 : Turn on the indicator light
92
87
  ### Device_ID, Command, Payload
93
- await client.send_api_request(123456789, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
88
+ await client.send_api_request(device_id, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
94
89
 
95
90
  ### Example 2 : Feed the pet
96
91
  ### Device_ID, Command, Payload
97
- await client.send_api_request(123456789, FeederCommand.MANUAL_FEED, {"amount": 1})
92
+ # simple hopper :
93
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount": 1})
94
+ # dual hopper :
95
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount1": 2})
96
+ # or
97
+ await client.send_api_request(device_id, FeederCommand.MANUAL_FEED, {"amount2": 2})
98
98
 
99
99
  ### Example 3 : Start the cleaning process
100
100
  ### Device_ID, Command, Payload
101
- await client.send_api_request(123456789, LitterCommand.CONTROL_DEVICE, {LBAction.START: LBCommand.CLEANING})
101
+ await client.send_api_request(device_id, LitterCommand.CONTROL_DEVICE, {DeviceAction.START: LBCommand.CLEANING})
102
102
 
103
103
 
104
104
  if __name__ == "__main__":
105
105
  asyncio.run(main())
106
106
  ```
107
107
 
108
+ ## More example usage
109
+
110
+ Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
111
+
108
112
  ## Contributing
109
113
 
110
114
  Contributions are welcome! Please open an issue or submit a pull request.
@@ -112,4 +116,3 @@ Contributions are welcome! Please open an issue or submit a pull request.
112
116
  ## License
113
117
 
114
118
  This project is licensed under the MIT License. See the LICENSE file for details.
115
-
@@ -0,0 +1,107 @@
1
+ """Pypetkit: A Python library for interfacing with PetKit"""
2
+
3
+ from .client import PetKitClient
4
+ from .command import (
5
+ DeviceAction,
6
+ DeviceCommand,
7
+ FeederCommand,
8
+ LBCommand,
9
+ LitterCommand,
10
+ PetCommand,
11
+ PurMode,
12
+ )
13
+ from .const import (
14
+ CTW3,
15
+ D3,
16
+ D4,
17
+ D4H,
18
+ D4S,
19
+ D4SH,
20
+ DEVICES_FEEDER,
21
+ DEVICES_LITTER_BOX,
22
+ DEVICES_PURIFIER,
23
+ DEVICES_WATER_FOUNTAIN,
24
+ FEEDER,
25
+ FEEDER_MINI,
26
+ FEEDER_WITH_CAMERA,
27
+ K2,
28
+ K3,
29
+ LITTER_NO_CAMERA,
30
+ LITTER_WITH_CAMERA,
31
+ T3,
32
+ T4,
33
+ T5,
34
+ T6,
35
+ W5,
36
+ MediaType,
37
+ RecordType,
38
+ )
39
+ from .containers import Pet
40
+ from .exceptions import (
41
+ PetkitAuthenticationUnregisteredEmailError,
42
+ PetkitRegionalServerNotFoundError,
43
+ PetkitSessionError,
44
+ PetkitSessionExpiredError,
45
+ PetkitTimeoutError,
46
+ PypetkitError,
47
+ )
48
+ from .feeder_container import Feeder, RecordsItems
49
+ from .litter_container import Litter, LitterRecord, WorkState
50
+ from .media import DownloadDecryptMedia, MediaCloud, MediaFile, MediaManager
51
+ from .purifier_container import Purifier
52
+ from .water_fountain_container import WaterFountain
53
+
54
+ __version__ = "1.11.2"
55
+
56
+ __all__ = [
57
+ "CTW3",
58
+ "D3",
59
+ "D4",
60
+ "D4H",
61
+ "D4S",
62
+ "D4SH",
63
+ "DEVICES_FEEDER",
64
+ "DEVICES_LITTER_BOX",
65
+ "DEVICES_PURIFIER",
66
+ "DEVICES_WATER_FOUNTAIN",
67
+ "FEEDER_WITH_CAMERA",
68
+ "LITTER_WITH_CAMERA",
69
+ "LITTER_NO_CAMERA",
70
+ "DeviceAction",
71
+ "DeviceCommand",
72
+ "FEEDER",
73
+ "FEEDER_MINI",
74
+ "Feeder",
75
+ "FeederCommand",
76
+ "K2",
77
+ "K3",
78
+ "LBCommand",
79
+ "Litter",
80
+ "LitterCommand",
81
+ "LitterRecord",
82
+ "MediaManager",
83
+ "DownloadDecryptMedia",
84
+ "MediaCloud",
85
+ "MediaFile",
86
+ "MediaType",
87
+ "Pet",
88
+ "PetCommand",
89
+ "PetKitClient",
90
+ "PetkitAuthenticationUnregisteredEmailError",
91
+ "PetkitRegionalServerNotFoundError",
92
+ "PetkitSessionError",
93
+ "PetkitSessionExpiredError",
94
+ "PetkitTimeoutError",
95
+ "PurMode",
96
+ "Purifier",
97
+ "PypetkitError",
98
+ "RecordType",
99
+ "RecordsItems",
100
+ "T3",
101
+ "T4",
102
+ "T5",
103
+ "T6",
104
+ "W5",
105
+ "WaterFountain",
106
+ "WorkState",
107
+ ]
@@ -0,0 +1,194 @@
1
+ """Module for handling Bluetooth communication with PetKit devices."""
2
+
3
+ import asyncio
4
+ import base64
5
+ from datetime import datetime
6
+ from http import HTTPMethod
7
+ import logging
8
+ from typing import TYPE_CHECKING
9
+ import urllib.parse
10
+
11
+ from pypetkitapi.command import FOUNTAIN_COMMAND, FountainAction
12
+ from pypetkitapi.const import (
13
+ BLE_CONNECT_ATTEMPT,
14
+ BLE_END_TRAME,
15
+ BLE_START_TRAME,
16
+ PetkitEndpoint,
17
+ )
18
+ from pypetkitapi.containers import BleRelay
19
+
20
+ if TYPE_CHECKING:
21
+ from pypetkitapi import PetKitClient, WaterFountain
22
+
23
+ _LOGGER = logging.getLogger(__name__)
24
+
25
+
26
+ class BluetoothManager:
27
+ """Class for handling Bluetooth communication with PetKit devices."""
28
+
29
+ def __init__(self, client: "PetKitClient"):
30
+ """Initialize the BluetoothManager class."""
31
+ self.client = client
32
+
33
+ async def _get_fountain_instance(self, fountain_id: int) -> "WaterFountain":
34
+ """Get the WaterFountain instance for the given fountain_id."""
35
+ from pypetkitapi.water_fountain_container import WaterFountain
36
+
37
+ water_fountain = self.client.petkit_entities.get(fountain_id)
38
+ if not isinstance(water_fountain, WaterFountain):
39
+ _LOGGER.error("Water fountain with ID %s not found.", fountain_id)
40
+ raise TypeError(f"Water fountain with ID {fountain_id} not found.")
41
+ return water_fountain
42
+
43
+ async def check_relay_availability(self, fountain_id: int) -> bool:
44
+ """Check if BLE relay is available for the given fountain_id."""
45
+ fountain = None
46
+ for account in self.client.account_data:
47
+ if account.device_list:
48
+ fountain = next(
49
+ (
50
+ device
51
+ for device in account.device_list
52
+ if device.device_id == fountain_id
53
+ ),
54
+ None,
55
+ )
56
+ if fountain:
57
+ break
58
+ if not fountain:
59
+ raise ValueError(
60
+ f"Fountain with device_id {fountain_id} not found for the current account"
61
+ )
62
+ group_id = fountain.group_id
63
+ response = await self.client.req.request(
64
+ method=HTTPMethod.POST,
65
+ url=f"{PetkitEndpoint.BLE_AS_RELAY}",
66
+ params={"groupId": group_id},
67
+ headers=await self.client.get_session_id(),
68
+ )
69
+ ble_relays = [BleRelay(**relay) for relay in response]
70
+ if len(ble_relays) == 0:
71
+ _LOGGER.warning("No BLE relay devices found.")
72
+ return False
73
+ return True
74
+
75
+ async def open_ble_connection(self, fountain_id: int) -> bool:
76
+ """Open a BLE connection to the given fountain_id."""
77
+ _LOGGER.info("Opening BLE connection to fountain %s", fountain_id)
78
+ water_fountain = await self._get_fountain_instance(fountain_id)
79
+ if await self.check_relay_availability(fountain_id) is False:
80
+ _LOGGER.error("BLE relay not available (id: %s).", fountain_id)
81
+ return False
82
+ if water_fountain.is_connected is True:
83
+ _LOGGER.error("BLE connection already established (id %s)", fountain_id)
84
+ return True
85
+ response = await self.client.req.request(
86
+ method=HTTPMethod.POST,
87
+ url=PetkitEndpoint.BLE_CONNECT,
88
+ data={"bleId": fountain_id, "type": 24, "mac": water_fountain.mac},
89
+ headers=await self.client.get_session_id(),
90
+ )
91
+ if response != {"state": 1}:
92
+ _LOGGER.error("Unable to open a BLE connection (id %s)", fountain_id)
93
+ water_fountain.is_connected = False
94
+ return False
95
+ for attempt in range(BLE_CONNECT_ATTEMPT):
96
+ _LOGGER.debug(
97
+ "BLE connection... %s/%s (id %s)",
98
+ attempt,
99
+ BLE_CONNECT_ATTEMPT,
100
+ fountain_id,
101
+ )
102
+ response = await self.client.req.request(
103
+ method=HTTPMethod.POST,
104
+ url=PetkitEndpoint.BLE_POLL,
105
+ data={"bleId": fountain_id, "type": 24, "mac": water_fountain.mac},
106
+ headers=await self.client.get_session_id(),
107
+ )
108
+ if response == 1:
109
+ _LOGGER.info(
110
+ "BLE connection established successfully (id %s)", fountain_id
111
+ )
112
+ water_fountain.is_connected = True
113
+ water_fountain.last_ble_poll = datetime.now().strftime(
114
+ "%Y-%m-%dT%H:%M:%S.%f"
115
+ )
116
+ return True
117
+ await asyncio.sleep(4)
118
+ _LOGGER.error(
119
+ "Failed to establish BLE connection after %s attempts (id %s)",
120
+ BLE_CONNECT_ATTEMPT,
121
+ fountain_id,
122
+ )
123
+ water_fountain.is_connected = False
124
+ return False
125
+
126
+ async def close_ble_connection(self, fountain_id: int) -> None:
127
+ """Close the BLE connection to the given fountain_id."""
128
+ _LOGGER.info("Closing BLE connection to fountain %s", fountain_id)
129
+ water_fountain = await self._get_fountain_instance(fountain_id)
130
+
131
+ if water_fountain.is_connected is False:
132
+ _LOGGER.error(
133
+ "BLE connection not established. Cannot close (id %s)", fountain_id
134
+ )
135
+ return
136
+
137
+ await self.client.req.request(
138
+ method=HTTPMethod.POST,
139
+ url=PetkitEndpoint.BLE_CANCEL,
140
+ data={"bleId": fountain_id, "type": 24, "mac": water_fountain.mac},
141
+ headers=await self.client.get_session_id(),
142
+ )
143
+ _LOGGER.info("BLE connection closed successfully (id %s)", fountain_id)
144
+
145
+ async def get_ble_cmd_data(
146
+ self, fountain_command: list, counter: int
147
+ ) -> tuple[int, str]:
148
+ """Get the BLE command data for the given fountain_command."""
149
+ cmd_code = fountain_command[0]
150
+ modified_command = fountain_command[:2] + [counter] + fountain_command[2:]
151
+ ble_data = [*BLE_START_TRAME, *modified_command, *BLE_END_TRAME]
152
+ encoded_data = await self._encode_ble_data(ble_data)
153
+ return cmd_code, encoded_data
154
+
155
+ @staticmethod
156
+ async def _encode_ble_data(byte_list: list) -> str:
157
+ """Encode the given byte_list to a base64 encoded string."""
158
+ byte_array = bytearray(byte_list)
159
+ b64_encoded = base64.b64encode(byte_array)
160
+ return urllib.parse.quote(b64_encoded)
161
+
162
+ async def send_ble_command(self, fountain_id: int, command: FountainAction) -> bool:
163
+ """Send the given BLE command to the fountain_id."""
164
+ _LOGGER.info("Sending BLE command to fountain %s", fountain_id)
165
+ water_fountain = await self._get_fountain_instance(fountain_id)
166
+ if water_fountain.is_connected is False:
167
+ _LOGGER.error("BLE connection not established (id %s)", fountain_id)
168
+ return False
169
+ command_data = FOUNTAIN_COMMAND.get(command)
170
+ if command_data is None:
171
+ _LOGGER.error(
172
+ "BLE fountain command '%s' not found (id %s)", command, fountain_id
173
+ )
174
+ return False
175
+ cmd_code, cmd_data = await self.get_ble_cmd_data(
176
+ list(command_data), water_fountain.ble_counter
177
+ )
178
+ response = await self.client.req.request(
179
+ method=HTTPMethod.POST,
180
+ url=PetkitEndpoint.BLE_CONTROL_DEVICE,
181
+ data={
182
+ "bleId": water_fountain.id,
183
+ "cmd": cmd_code,
184
+ "data": cmd_data,
185
+ "mac": water_fountain.mac,
186
+ "type": 24,
187
+ },
188
+ headers=await self.client.get_session_id(),
189
+ )
190
+ if response != 1:
191
+ _LOGGER.error("Failed to send BLE command (id %s)", fountain_id)
192
+ return False
193
+ _LOGGER.info("BLE command sent successfully (id %s)", fountain_id)
194
+ return True