pypetkitapi 1.6.2__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,21 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: pypetkitapi
3
- Version: 1.6.2
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
- Requires-Dist: pycryptodome (>=3.21.0,<4.0.0)
17
- Description-Content-Type: text/markdown
18
-
19
1
  # Petkit API Client
20
2
 
21
3
  ---
@@ -30,6 +12,12 @@ Description-Content-Type: text/markdown
30
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)
31
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)
32
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)
33
21
 
34
22
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)][pre-commit]
35
23
  [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)][black]
@@ -64,9 +52,11 @@ pip install pypetkitapi
64
52
  ## Usage Example:
65
53
 
66
54
  ```python
55
+ import asyncio
56
+ import logging
67
57
  import aiohttp
68
58
  from pypetkitapi.client import PetKitClient
69
- from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, LBAction, LitterCommand
59
+ from pypetkitapi.command import DeviceCommand, FeederCommand, LBCommand, DeviceAction, LitterCommand
70
60
 
71
61
  logging.basicConfig(level=logging.DEBUG)
72
62
 
@@ -75,37 +65,50 @@ async def main():
75
65
  client = PetKitClient(
76
66
  username="username", # Your PetKit account username or id
77
67
  password="password", # Your PetKit account password
78
- region="FR", # Your region or country code (e.g. FR, US, etc.)
79
- 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")
80
70
  session=session,
81
71
  )
82
72
 
83
73
  await client.get_devices_data()
84
74
 
85
- # Read the account data
86
- 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}")
87
79
 
88
- # Read the devices data
89
- 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])
90
84
 
91
85
  # Send command to the devices
92
86
  ### Example 1 : Turn on the indicator light
93
87
  ### Device_ID, Command, Payload
94
- await client.send_api_request(123456789, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
88
+ await client.send_api_request(device_id, DeviceCommand.UPDATE_SETTING, {"lightMode": 1})
95
89
 
96
90
  ### Example 2 : Feed the pet
97
91
  ### Device_ID, Command, Payload
98
- 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})
99
98
 
100
99
  ### Example 3 : Start the cleaning process
101
100
  ### Device_ID, Command, Payload
102
- 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})
103
102
 
104
103
 
105
104
  if __name__ == "__main__":
106
105
  asyncio.run(main())
107
106
  ```
108
107
 
108
+ ## More example usage
109
+
110
+ Check at the usage in the Home Assistant integration : [here](https://github.com/Jezza34000/homeassistant_petkit)
111
+
109
112
  ## Contributing
110
113
 
111
114
  Contributions are welcome! Please open an issue or submit a pull request.
@@ -113,4 +116,3 @@ Contributions are welcome! Please open an issue or submit a pull request.
113
116
  ## License
114
117
 
115
118
  This project is licensed under the MIT License. See the LICENSE file for details.
116
-
@@ -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