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.
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/LICENSE +1 -1
- pypetkitapi-1.2.0/README.md → pypetkitapi-1.11.2/PKG-INFO +53 -10
- pypetkitapi-1.2.0/PKG-INFO → pypetkitapi-1.11.2/README.md +31 -28
- pypetkitapi-1.11.2/pypetkitapi/__init__.py +107 -0
- pypetkitapi-1.11.2/pypetkitapi/bluetooth.py +194 -0
- pypetkitapi-1.11.2/pypetkitapi/client.py +729 -0
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/command.py +86 -77
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/const.py +87 -19
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/containers.py +41 -5
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/exceptions.py +14 -1
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/feeder_container.py +111 -54
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/litter_container.py +243 -20
- pypetkitapi-1.11.2/pypetkitapi/media.py +679 -0
- pypetkitapi-1.11.2/pypetkitapi/purifier_container.py +77 -0
- pypetkitapi-1.11.2/pypetkitapi/schedule_container.py +67 -0
- pypetkitapi-1.11.2/pypetkitapi/utils.py +22 -0
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/water_fountain_container.py +24 -14
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pyproject.toml +28 -8
- pypetkitapi-1.2.0/pypetkitapi/__init__.py +0 -1
- pypetkitapi-1.2.0/pypetkitapi/client.py +0 -466
- {pypetkitapi-1.2.0 → pypetkitapi-1.11.2}/pypetkitapi/py.typed +0 -0
@@ -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
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
13
34
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
14
35
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
36
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
37
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
38
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
39
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
40
|
+
|
41
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
15
42
|
|
16
43
|
[][pre-commit]
|
17
44
|
[][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,
|
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
|
-
#
|
68
|
-
|
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
|
-
#
|
71
|
-
|
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(
|
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
|
-
|
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(
|
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
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
30
13
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
31
14
|
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
15
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
16
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
17
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
18
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
19
|
+
|
20
|
+
[](https://sonarcloud.io/summary/new_code?id=Jezza34000_py-petkit-api)
|
32
21
|
|
33
22
|
[][pre-commit]
|
34
23
|
[][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,
|
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
|
-
#
|
85
|
-
|
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
|
-
#
|
88
|
-
|
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(
|
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
|
-
|
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(
|
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
|