homebridge-kasa-python 2.6.13 → 2.7.0-beta.0
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.
- package/dist/config.d.ts +3 -3
- package/dist/devices/deviceManager.d.ts +3 -1
- package/dist/devices/deviceManager.js +39 -27
- package/dist/devices/deviceManager.js.map +1 -1
- package/dist/devices/homekitPlug.d.ts +0 -8
- package/dist/devices/homekitPlug.js +0 -121
- package/dist/devices/homekitPlug.js.map +1 -1
- package/dist/devices/homekitPowerstrip.d.ts +0 -8
- package/dist/devices/homekitPowerstrip.js +0 -144
- package/dist/devices/homekitPowerstrip.js.map +1 -1
- package/dist/devices/homekitSwitch.d.ts +1 -8
- package/dist/devices/homekitSwitch.js +9 -120
- package/dist/devices/homekitSwitch.js.map +1 -1
- package/dist/devices/index.d.ts +12 -2
- package/dist/devices/index.js +208 -1
- package/dist/devices/index.js.map +1 -1
- package/dist/devices/kasaDevices.d.ts +23 -3
- package/dist/devices/kasaDevices.js.map +1 -1
- package/dist/platform.js +2 -2
- package/dist/platform.js.map +1 -1
- package/dist/python/kasaApi.py +114 -64
- package/package.json +10 -10
- package/requirements.txt +2 -2
package/dist/python/kasaApi.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import asyncio, eventlet, eventlet.wsgi, os, requests, sys
|
|
1
|
+
import asyncio, eventlet, eventlet.wsgi, math, os, requests, sys
|
|
2
2
|
from flask import Flask, request, jsonify
|
|
3
3
|
from flask_socketio import SocketIO
|
|
4
|
-
from kasa import Credentials, Discover, Device,
|
|
4
|
+
from kasa import Credentials, Discover, Device, Module, UnsupportedDeviceException
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
7
|
app = Flask(__name__)
|
|
@@ -21,17 +21,26 @@ logger.add(RemoteLogger(logging_server_url), level="DEBUG")
|
|
|
21
21
|
|
|
22
22
|
app.logger = logger
|
|
23
23
|
|
|
24
|
-
device_cache = {}
|
|
25
|
-
|
|
26
24
|
UNSUPPORTED_TYPES = {
|
|
27
25
|
'SMART.IPCAMERA',
|
|
28
|
-
'IOT.SMARTBULB',
|
|
29
26
|
'SMART.KASAHUB',
|
|
30
|
-
'SMART.TAPOBULB',
|
|
31
27
|
'SMART.TAPOHUB'
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
def
|
|
30
|
+
def serialize_child(child: Device):
|
|
31
|
+
return {
|
|
32
|
+
"alias": child.alias,
|
|
33
|
+
**({"brightness": getattr(child.modules[Module.Light], "brightness")} if Module.Light in child.modules else {}),
|
|
34
|
+
**({"color_temp": getattr(child.modules[Module.Light], "color_temp")} if Module.Light in child.modules else {}),
|
|
35
|
+
**({"hsv": {
|
|
36
|
+
"hue": getattr(child.modules[Module.Light], "hsv")[0],
|
|
37
|
+
"saturation": getattr(child.modules[Module.Light], "hsv")[1]
|
|
38
|
+
}} if Module.Light in child.modules else {}),
|
|
39
|
+
"id": child.device_id.split("_", 1)[1],
|
|
40
|
+
"state": child.features["state"].value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
def custom_sysinfo_config_serializer(device: Device):
|
|
35
44
|
app.logger.debug(f"Serializing device: {device.host}")
|
|
36
45
|
|
|
37
46
|
child_num = len(device.children) if device.children else 0
|
|
@@ -43,26 +52,31 @@ def custom_device_serializer(device: Device):
|
|
|
43
52
|
"device_type": device.config.connection_type.device_family.value,
|
|
44
53
|
"host": device.host,
|
|
45
54
|
"hw_ver": device.hw_info["hw_ver"],
|
|
46
|
-
"is_off": device.is_off,
|
|
47
|
-
"is_on": device.is_on,
|
|
48
55
|
"mac": device.hw_info["mac"],
|
|
49
56
|
"sw_ver": device.sys_info.get("sw_ver") or device.sys_info.get("fw_ver")
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
if child_num > 0:
|
|
53
|
-
sys_info["children"] = [
|
|
54
|
-
{
|
|
55
|
-
"id": child.device_id.split("_", 1)[1],
|
|
56
|
-
"state": child.features["state"].value,
|
|
57
|
-
"alias": child.alias
|
|
58
|
-
} for child in device.children
|
|
59
|
-
]
|
|
60
|
+
sys_info["children"] = [serialize_child(child) for child in device.children]
|
|
60
61
|
else:
|
|
61
|
-
sys_info
|
|
62
|
+
sys_info.update({
|
|
63
|
+
"state": device.features["state"].value,
|
|
64
|
+
**({"brightness": getattr(device.modules[Module.Light], "brightness")} if Module.Light in device.modules else {}),
|
|
65
|
+
**({"color_temp": getattr(device.modules[Module.Light], "color_temp")} if Module.Light in device.modules else {}),
|
|
66
|
+
**({"hsv": {
|
|
67
|
+
"hue": getattr(device.modules[Module.Light], "hsv")[0],
|
|
68
|
+
"saturation": getattr(device.modules[Module.Light], "hsv")[1]
|
|
69
|
+
}} if Module.Light in device.modules else {})
|
|
70
|
+
})
|
|
62
71
|
|
|
63
72
|
device_config = {
|
|
64
73
|
"host": device.config.host,
|
|
65
74
|
"timeout": device.config.timeout,
|
|
75
|
+
**({"credentials": {
|
|
76
|
+
"username": device.config.credentials.username,
|
|
77
|
+
"password": device.config.credentials.password
|
|
78
|
+
}} if device.config.credentials else {
|
|
79
|
+
}),
|
|
66
80
|
"connection_type": {
|
|
67
81
|
"device_family": device.config.connection_type.device_family.value,
|
|
68
82
|
"encryption_type": device.config.connection_type.encryption_type.value,
|
|
@@ -71,25 +85,27 @@ def custom_device_serializer(device: Device):
|
|
|
71
85
|
"uses_http": device.config.uses_http
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
if device.config.credentials:
|
|
75
|
-
device_config["credentials"] = {
|
|
76
|
-
"username": device.config.credentials.username,
|
|
77
|
-
"password": device.config.credentials.password
|
|
78
|
-
}
|
|
79
|
-
|
|
80
88
|
return {
|
|
81
89
|
"sys_info": sys_info,
|
|
82
90
|
"device_config": device_config
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
def
|
|
93
|
+
def custom_discovery_feature_serializer(device: Device):
|
|
86
94
|
app.logger.debug(f"Serializing device for discovery: {device.host}")
|
|
87
95
|
disc_info = {
|
|
88
96
|
"model": device._discovery_info.get("device_model", device.sys_info.get("model"))
|
|
89
97
|
}
|
|
90
98
|
|
|
99
|
+
app.logger.debug(f"Serializing device features: {device.host}")
|
|
100
|
+
feature_info = {
|
|
101
|
+
**({"brightness": device.modules[Module.Light].is_dimmable} if Module.Light in device.modules else {}),
|
|
102
|
+
**({"color_temp": device.modules[Module.Light].is_variable_color_temp} if Module.Light in device.modules else {}),
|
|
103
|
+
**({"hsv": device.modules[Module.Light].is_color} if Module.Light in device.modules else {})
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
return {
|
|
92
|
-
"disc_info": disc_info
|
|
107
|
+
"disc_info": disc_info,
|
|
108
|
+
"feature_info": feature_info
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
async def discover_devices(username=None, password=None, additional_broadcasts=None, manual_devices=None):
|
|
@@ -101,8 +117,8 @@ async def discover_devices(username=None, password=None, additional_broadcasts=N
|
|
|
101
117
|
async def on_discovered(device: Device):
|
|
102
118
|
try:
|
|
103
119
|
await device.update()
|
|
104
|
-
app.logger.debug(f"Discovered device: {device.host}")
|
|
105
|
-
except
|
|
120
|
+
app.logger.debug(f"Discovered device has been updated: {device.host}")
|
|
121
|
+
except UnsupportedDeviceException as e:
|
|
106
122
|
app.logger.warning(f"Unsupported device found during discovery: {device.host} - {str(e)}")
|
|
107
123
|
except Exception as e:
|
|
108
124
|
app.logger.error(f"Error updating device during discovery: {device.host} - {str(e)}")
|
|
@@ -112,26 +128,27 @@ async def discover_devices(username=None, password=None, additional_broadcasts=N
|
|
|
112
128
|
app.logger.debug(f"Starting discovery on broadcast: {broadcast}")
|
|
113
129
|
discovered_devices = await Discover.discover(target=broadcast, credentials=creds, on_discovered=on_discovered)
|
|
114
130
|
for ip, dev in discovered_devices.items():
|
|
115
|
-
|
|
116
|
-
|
|
131
|
+
if ip not in devices:
|
|
132
|
+
devices[ip] = dev
|
|
133
|
+
app.logger.debug(f"Added device {ip} from broadcast {broadcast} to devices list")
|
|
117
134
|
app.logger.debug(f"Discovered {len(discovered_devices)} devices on broadcast {broadcast}")
|
|
118
135
|
except Exception as e:
|
|
119
136
|
app.logger.error(f"Error processing broadcast {broadcast}: {str(e)}", exc_info=True)
|
|
120
137
|
|
|
121
138
|
async def discover_manual_device(host):
|
|
122
139
|
if host in devices:
|
|
123
|
-
app.logger.debug(f"
|
|
140
|
+
app.logger.debug(f"Device {host} already exists in devices list, skipping.")
|
|
124
141
|
return
|
|
125
142
|
try:
|
|
126
|
-
app.logger.debug(f"
|
|
143
|
+
app.logger.debug(f"Starting discovery for device: {host}")
|
|
127
144
|
discovered_device = await Discover.discover_single(host=host, credentials=creds)
|
|
128
|
-
await discovered_device
|
|
145
|
+
await on_discovered(discovered_device)
|
|
129
146
|
devices[host] = discovered_device
|
|
130
|
-
app.logger.debug(f"Discovered
|
|
131
|
-
except
|
|
132
|
-
app.logger.warning(f"Unsupported device found during
|
|
147
|
+
app.logger.debug(f"Discovered device: {host}")
|
|
148
|
+
except UnsupportedDeviceException as e:
|
|
149
|
+
app.logger.warning(f"Unsupported device found during discovery: {host} - {str(e)}")
|
|
133
150
|
except Exception as e:
|
|
134
|
-
app.logger.error(f"Error discovering
|
|
151
|
+
app.logger.error(f"Error discovering device {host}: {str(e)}", exc_info=True)
|
|
135
152
|
|
|
136
153
|
await asyncio.gather(*(discover_on_broadcast(broadcast) for broadcast in broadcasts))
|
|
137
154
|
await asyncio.gather(*(discover_manual_device(host) for host in manual_devices))
|
|
@@ -145,7 +162,7 @@ async def discover_devices(username=None, password=None, additional_broadcasts=N
|
|
|
145
162
|
components = await dev._raw_query("component_nego")
|
|
146
163
|
component_list = components.get("component_nego", {}).get("component_list", [])
|
|
147
164
|
homekit_component = next((item for item in component_list if item.get("id") == "homekit"), None)
|
|
148
|
-
if
|
|
165
|
+
if homekit_component:
|
|
149
166
|
app.logger.debug(f"Native HomeKit Support found for device {ip} and was not added to update tasks")
|
|
150
167
|
continue
|
|
151
168
|
except Exception:
|
|
@@ -154,10 +171,10 @@ async def discover_devices(username=None, password=None, additional_broadcasts=N
|
|
|
154
171
|
try:
|
|
155
172
|
dev_type = dev.sys_info.get("mic_type") or dev.sys_info.get("type")
|
|
156
173
|
if dev_type and dev_type not in UNSUPPORTED_TYPES:
|
|
157
|
-
tasks.append(
|
|
158
|
-
app.logger.debug(f"Device {ip}
|
|
174
|
+
tasks.append(create_device_info(ip, dev))
|
|
175
|
+
app.logger.debug(f"Device {ip} added to update tasks")
|
|
159
176
|
else:
|
|
160
|
-
app.logger.debug(f"Device {ip}
|
|
177
|
+
app.logger.debug(f"Device {ip} is unsupported and was not added to update tasks")
|
|
161
178
|
except Exception as e:
|
|
162
179
|
app.logger.error(f"Error adding device {ip} to update tasks: {str(e)}")
|
|
163
180
|
|
|
@@ -173,23 +190,26 @@ async def discover_devices(username=None, password=None, additional_broadcasts=N
|
|
|
173
190
|
app.logger.debug(f"Device discovery completed with {len(all_device_info)} devices found")
|
|
174
191
|
return all_device_info
|
|
175
192
|
|
|
176
|
-
async def
|
|
177
|
-
|
|
193
|
+
async def create_device_info(ip, dev: Device):
|
|
194
|
+
created_device_info = {}
|
|
195
|
+
app.logger.debug(f"Creating device info for {ip}")
|
|
178
196
|
try:
|
|
179
|
-
|
|
180
|
-
sys_info =
|
|
181
|
-
device_config =
|
|
182
|
-
|
|
183
|
-
disc_info =
|
|
184
|
-
|
|
197
|
+
device_info = custom_sysinfo_config_serializer(dev)
|
|
198
|
+
sys_info = device_info["sys_info"]
|
|
199
|
+
device_config = device_info["device_config"]
|
|
200
|
+
device_info = custom_discovery_feature_serializer(dev)
|
|
201
|
+
disc_info = device_info["disc_info"]
|
|
202
|
+
feature_info = device_info["feature_info"]
|
|
203
|
+
created_device_info[ip] = {
|
|
185
204
|
"sys_info": sys_info,
|
|
186
205
|
"disc_info": disc_info,
|
|
206
|
+
"feature_info": feature_info,
|
|
187
207
|
"device_config": device_config
|
|
188
208
|
}
|
|
189
|
-
app.logger.debug(f"
|
|
190
|
-
return ip,
|
|
209
|
+
app.logger.debug(f"Created device info for {ip}")
|
|
210
|
+
return ip, created_device_info[ip]
|
|
191
211
|
except Exception as e:
|
|
192
|
-
app.logger.error(f"Error
|
|
212
|
+
app.logger.error(f"Error creating device info for {ip}: {str(e)}")
|
|
193
213
|
return ip, {}
|
|
194
214
|
|
|
195
215
|
async def get_sys_info(device_config):
|
|
@@ -200,7 +220,7 @@ async def get_sys_info(device_config):
|
|
|
200
220
|
app.logger.warning(f"Alias not found for device {dev.host}. Reconnecting and updating...")
|
|
201
221
|
await dev.disconnect()
|
|
202
222
|
dev = await Device.connect(config=Device.Config.from_dict(device_config))
|
|
203
|
-
device =
|
|
223
|
+
device = custom_sysinfo_config_serializer(dev)
|
|
204
224
|
sys_info = device["sys_info"]
|
|
205
225
|
return {"sys_info": sys_info}
|
|
206
226
|
except Exception as e:
|
|
@@ -209,27 +229,55 @@ async def get_sys_info(device_config):
|
|
|
209
229
|
finally:
|
|
210
230
|
await dev.disconnect()
|
|
211
231
|
|
|
212
|
-
async def control_device(device_config, action, child_num=None):
|
|
213
|
-
if child_num is not None
|
|
214
|
-
app.logger.debug(f"Controlling device: {device_config['host']}, action: {action}, child_num: {child_num}")
|
|
215
|
-
else:
|
|
216
|
-
app.logger.debug(f"Controlling device: {device_config['host']}, action: {action}")
|
|
232
|
+
async def control_device(device_config, feature, action, value, child_num=None):
|
|
233
|
+
app.logger.debug(f"Controlling device: {device_config['host']}, feature: {feature}, action: {action}, child_num: {child_num if child_num is not None else 'N/A'}")
|
|
217
234
|
|
|
218
235
|
dev = await Device.connect(config=Device.Config.from_dict(device_config))
|
|
219
236
|
try:
|
|
220
|
-
if child_num is not None
|
|
221
|
-
|
|
222
|
-
|
|
237
|
+
target = dev.children[child_num] if child_num is not None else dev
|
|
238
|
+
|
|
239
|
+
if feature == "state":
|
|
240
|
+
await getattr(target, action)()
|
|
241
|
+
elif feature == "brightness":
|
|
242
|
+
await handle_brightness(target, action, value)
|
|
243
|
+
elif feature == "color_temp":
|
|
244
|
+
await handle_color_temp(target, action, value)
|
|
245
|
+
elif feature in ["hue", "saturation"]:
|
|
246
|
+
await handle_hsv(target, action, feature, value)
|
|
223
247
|
else:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
248
|
+
raise Exception(f"Unsupported feature: {feature}")
|
|
249
|
+
|
|
250
|
+
app.logger.debug(f"Controlled device {device_config['host']} with feature: {feature}, action: {action}, child_num: {child_num if child_num is not None else 'N/A'}")
|
|
251
|
+
return {"status": "success"}
|
|
227
252
|
except Exception as e:
|
|
228
253
|
app.logger.error(f"Error controlling device {device_config['host']}: {str(e)}")
|
|
229
254
|
return {"status": "error", "message": str(e)}
|
|
230
255
|
finally:
|
|
231
256
|
await dev.disconnect()
|
|
232
257
|
|
|
258
|
+
async def handle_brightness(target: Device, action, value):
|
|
259
|
+
if value == 0:
|
|
260
|
+
await target.turn_off()
|
|
261
|
+
elif value > 0 and value < 100:
|
|
262
|
+
light = target.modules[Module.Light]
|
|
263
|
+
await getattr(light, action)(value)
|
|
264
|
+
else:
|
|
265
|
+
await target.turn_on()
|
|
266
|
+
|
|
267
|
+
async def handle_color_temp(target: Device, action, value):
|
|
268
|
+
light = target.modules[Module.Light]
|
|
269
|
+
range = light.valid_temperature_range
|
|
270
|
+
if value < range[0]:
|
|
271
|
+
value = range[0]
|
|
272
|
+
elif value > range[1]:
|
|
273
|
+
value = range[1]
|
|
274
|
+
await getattr(light, action)(value)
|
|
275
|
+
|
|
276
|
+
async def handle_hsv(target: Device, action, feature, value):
|
|
277
|
+
light = target.modules[Module.Light]
|
|
278
|
+
hsv_value = {feature: value[feature]}
|
|
279
|
+
await getattr(light, action)(hsv_value)
|
|
280
|
+
|
|
233
281
|
def run_async(func, *args):
|
|
234
282
|
loop = asyncio.get_event_loop()
|
|
235
283
|
return loop.run_until_complete(func(*args))
|
|
@@ -266,10 +314,12 @@ def control_device_route():
|
|
|
266
314
|
device_config = data['device_config']
|
|
267
315
|
credentials = device_config.get('credentials')
|
|
268
316
|
device_config.update({'credentials': Credentials(username=credentials['username'], password=credentials['password'])} if credentials else {})
|
|
317
|
+
feature = data['feature']
|
|
269
318
|
action = data['action']
|
|
319
|
+
value = data['value']
|
|
270
320
|
child_num = data.get('child_num')
|
|
271
321
|
app.logger.debug(f"Controlling device: {device_config['host']}, action: {action}, child_num: {child_num}")
|
|
272
|
-
result = run_async(control_device, device_config, action, child_num)
|
|
322
|
+
result = run_async(control_device, device_config, feature, action, value, child_num)
|
|
273
323
|
return jsonify(result)
|
|
274
324
|
|
|
275
325
|
@app.route('/health', methods=['GET'])
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "Homebridge Kasa Python",
|
|
3
3
|
"name": "homebridge-kasa-python",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.7.0-beta.0",
|
|
5
5
|
"description": "Plugin that uses Python-Kasa API to communicate with Kasa Devices.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "https://github.com/ZeliardM/homebridge-kasa-python/issues"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"node": "^18.20.5 || ^20.18.1 || ^22.
|
|
16
|
+
"node": "^18.20.5 || ^20.18.1 || ^22.12.0 || ^23.3.0",
|
|
17
17
|
"homebridge": "^1.8.0 || ^2.0.0-beta.0",
|
|
18
18
|
"python": "^3.9.0"
|
|
19
19
|
},
|
|
@@ -47,20 +47,20 @@
|
|
|
47
47
|
],
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/eslintrc": "^3.2.0",
|
|
50
|
-
"@eslint/js": "^9.
|
|
50
|
+
"@eslint/js": "^9.16.0",
|
|
51
51
|
"@stylistic/eslint-plugin": "^2.11.0",
|
|
52
52
|
"@types/lodash.defaults": "^4.2.9",
|
|
53
|
-
"@types/node": "^22.
|
|
53
|
+
"@types/node": "^22.10.1",
|
|
54
54
|
"@types/semver": "^7.5.8",
|
|
55
|
-
"@typescript-eslint/parser": "^8.
|
|
56
|
-
"eslint": "^9.
|
|
57
|
-
"globals": "^15.
|
|
55
|
+
"@typescript-eslint/parser": "^8.17.0",
|
|
56
|
+
"eslint": "^9.16.0",
|
|
57
|
+
"globals": "^15.13.0",
|
|
58
58
|
"homebridge": "^2.0.0-beta.23",
|
|
59
59
|
"nodemon": "^3.1.7",
|
|
60
60
|
"node-persist": "^4.0.3",
|
|
61
61
|
"rimraf": "^6.0.1",
|
|
62
62
|
"ts-node": "^10.9.2",
|
|
63
|
-
"typescript-eslint": "^8.
|
|
63
|
+
"typescript-eslint": "^8.17.0"
|
|
64
64
|
},
|
|
65
65
|
"homepage": "https://github.com/ZeliardM/homebridge-kasa-python#readme",
|
|
66
66
|
"funding": [
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"ajv": "^8.17.1",
|
|
78
78
|
"ajv-formats": "^3.0.1",
|
|
79
|
-
"axios": "^1.7.
|
|
79
|
+
"axios": "^1.7.9",
|
|
80
80
|
"get-port": "^7.1.0",
|
|
81
81
|
"lodash.defaults": "^4.2.0",
|
|
82
82
|
"semver": "^7.6.3",
|
|
83
83
|
"ts-essentials": "^10.0.3",
|
|
84
|
-
"typescript": "^5.
|
|
84
|
+
"typescript": "^5.7.2",
|
|
85
85
|
"util": "^0.12.5"
|
|
86
86
|
},
|
|
87
87
|
"overrides": {
|
package/requirements.txt
CHANGED