mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
Add WallSwitch device type and autogenerate supported devices docs (#758)
This commit is contained in:
parent
0306e05fb9
commit
fcad0d2344
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@ -26,6 +26,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip poetry
|
python -m pip install --upgrade pip poetry
|
||||||
poetry install
|
poetry install
|
||||||
|
- name: "Check supported device md files are up to date"
|
||||||
|
run: |
|
||||||
|
poetry run pre-commit run generate-supported --all-files
|
||||||
- name: "Linting and code formating (ruff)"
|
- name: "Linting and code formating (ruff)"
|
||||||
run: |
|
run: |
|
||||||
poetry run pre-commit run ruff --all-files
|
poetry run pre-commit run ruff --all-files
|
||||||
@ -47,9 +50,6 @@ jobs:
|
|||||||
- name: "Run check-ast"
|
- name: "Run check-ast"
|
||||||
run: |
|
run: |
|
||||||
poetry run pre-commit run check-ast --all-files
|
poetry run pre-commit run check-ast --all-files
|
||||||
- name: "Check README for supported models"
|
|
||||||
run: |
|
|
||||||
poetry run python -m devtools.check_readme_vs_fixtures
|
|
||||||
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
|
@ -27,3 +27,14 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: doc8
|
- id: doc8
|
||||||
additional_dependencies: [tomli]
|
additional_dependencies: [tomli]
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: generate-supported
|
||||||
|
name: Generate supported devices
|
||||||
|
description: This hook generates the supported device sections of README.md and SUPPORTED.md
|
||||||
|
entry: devtools/generate_supported.py
|
||||||
|
language: system # Required or pre-commit creates a new venv
|
||||||
|
verbose: true # Show output on success
|
||||||
|
types: [json]
|
||||||
|
pass_filenames: false # passing filenames causes the hook to run in batches against all-files
|
||||||
|
128
README.md
128
README.md
@ -220,120 +220,32 @@ Note, that this works currently only on kasa-branded devices which use port 9999
|
|||||||
|
|
||||||
## Supported devices
|
## Supported devices
|
||||||
|
|
||||||
In principle, most kasa-branded devices that are locally controllable using the official Kasa mobile app work with this library.
|
The following devices have been tested and confirmed as working. If your device is unlisted but working, please open a pull request to update the list and add a fixture file (use `python -m devtools.dump_devinfo` to generate one).
|
||||||
|
|
||||||
The following lists the devices that have been manually verified to work.
|
<!--Do not edit text inside the SUPPORTED section below -->
|
||||||
**If your device is unlisted but working, please open a pull request to update the list and add a fixture file (use `python -m devtools.dump_devinfo` to generate one).**
|
<!--SUPPORTED_START-->
|
||||||
|
### Supported Kasa devices
|
||||||
|
|
||||||
### Plugs
|
- **Plugs**: EP10, EP25<sup>\*</sup>, HS100<sup>\*\*</sup>, HS103, HS105, HS110, KP100, KP105, KP115, KP125, KP125M<sup>\*</sup>, KP401
|
||||||
|
- **Power Strips**: EP40, HS107, HS300, KP200, KP303, KP400
|
||||||
|
- **Wall Switches**: ES20M, HS200, HS210, HS220, KP405, KS200M, KS205<sup>\*</sup>, KS220M, KS225<sup>\*</sup>, KS230
|
||||||
|
- **Bulbs**: KL110, KL120, KL125, KL130, KL135, KL50, KL60, LB100, LB110, LB120, LB130
|
||||||
|
- **Light Strips**: KL400L5, KL420L5, KL430
|
||||||
|
|
||||||
* HS100
|
### Supported Tapo<sup>\*</sup> devices
|
||||||
* HS103
|
|
||||||
* HS105
|
|
||||||
* HS107
|
|
||||||
* HS110
|
|
||||||
* KP100
|
|
||||||
* KP105
|
|
||||||
* KP115
|
|
||||||
* KP125
|
|
||||||
* KP125M [See note below](#newer-kasa-branded-devices)
|
|
||||||
* KP401
|
|
||||||
* EP10
|
|
||||||
* EP25 [See note below](#newer-kasa-branded-devices)
|
|
||||||
|
|
||||||
### Power Strips
|
- **Plugs**: P100, P110, P125M, P135, TP15
|
||||||
|
- **Power Strips**: P300, TP25
|
||||||
|
- **Wall Switches**: S500D, S505
|
||||||
|
- **Bulbs**: L510B, L510E, L530E
|
||||||
|
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
|
||||||
|
- **Hubs**: H100
|
||||||
|
|
||||||
* EP40
|
<!--SUPPORTED_END-->
|
||||||
* HS300
|
<sup>*</sup> Model requires authentication<br>
|
||||||
* KP303
|
<sup>**</sup> Newer versions require authentication
|
||||||
* KP200 (in wall)
|
|
||||||
* KP400
|
|
||||||
* KP405 (dimmer)
|
|
||||||
|
|
||||||
### Wall switches
|
See [supported devices in our documentation](SUPPORTED.md) for more detailed information about tested hardware and software versions.
|
||||||
|
|
||||||
* ES20M
|
|
||||||
* HS200
|
|
||||||
* HS210
|
|
||||||
* HS220
|
|
||||||
* KS200M (partial support, no motion, no daylight detection)
|
|
||||||
* KS220M (partial support, no motion, no daylight detection)
|
|
||||||
* KS230
|
|
||||||
|
|
||||||
### Bulbs
|
|
||||||
|
|
||||||
* LB100
|
|
||||||
* LB110
|
|
||||||
* LB120
|
|
||||||
* LB130
|
|
||||||
* LB230
|
|
||||||
* KL50
|
|
||||||
* KL60
|
|
||||||
* KL110
|
|
||||||
* KL120
|
|
||||||
* KL125
|
|
||||||
* KL130
|
|
||||||
* KL135
|
|
||||||
|
|
||||||
### Light strips
|
|
||||||
|
|
||||||
* KL400L5
|
|
||||||
* KL420L5
|
|
||||||
* KL430
|
|
||||||
|
|
||||||
### Tapo branded devices
|
|
||||||
|
|
||||||
The library has recently added a limited supported for devices that carry Tapo branding.
|
|
||||||
|
|
||||||
At the moment, the following devices have been confirmed to work:
|
|
||||||
|
|
||||||
#### Plugs
|
|
||||||
|
|
||||||
* Tapo P110
|
|
||||||
* Tapo P125M
|
|
||||||
* Tapo P135 (dimming not yet supported)
|
|
||||||
* Tapo TP15
|
|
||||||
|
|
||||||
#### Bulbs
|
|
||||||
|
|
||||||
* Tapo L510B
|
|
||||||
* Tapo L510E
|
|
||||||
* Tapo L530E
|
|
||||||
|
|
||||||
#### Light strips
|
|
||||||
|
|
||||||
* Tapo L900-5
|
|
||||||
* Tapo L900-10
|
|
||||||
* Tapo L920-5
|
|
||||||
* Tapo L930-5
|
|
||||||
|
|
||||||
#### Wall switches
|
|
||||||
|
|
||||||
* Tapo S500D
|
|
||||||
* Tapo S505
|
|
||||||
|
|
||||||
#### Power strips
|
|
||||||
|
|
||||||
* Tapo P300
|
|
||||||
* Tapo TP25
|
|
||||||
|
|
||||||
#### Hubs
|
|
||||||
|
|
||||||
* Tapo H100
|
|
||||||
|
|
||||||
### Newer Kasa branded devices
|
|
||||||
|
|
||||||
Some newer hardware versions of Kasa branded devices are now using the same protocol as
|
|
||||||
Tapo branded devices. Support for these devices is currently limited as per TAPO branded
|
|
||||||
devices:
|
|
||||||
|
|
||||||
* Kasa EP25 (plug) hw_version 2.6
|
|
||||||
* Kasa KP125M (plug)
|
|
||||||
* Kasa KS205 (Wifi/Matter Wall Switch)
|
|
||||||
* Kasa KS225 (Wifi/Matter Wall Dimmer Switch)
|
|
||||||
|
|
||||||
|
|
||||||
**If your device is unlisted but working, please open a pull request to update the list and add a fixture file (use `python -m devtools.dump_devinfo` to generate one).**
|
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
|
210
SUPPORTED.md
Normal file
210
SUPPORTED.md
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
# Supported devices
|
||||||
|
|
||||||
|
The following devices have been tested and confirmed as working. If your device is unlisted but working, please open a pull request to update the list and add a fixture file (use `python -m devtools.dump_devinfo` to generate one).
|
||||||
|
|
||||||
|
<!--Do not edit text inside the SUPPORTED section below -->
|
||||||
|
<!--SUPPORTED_START-->
|
||||||
|
## Kasa devices
|
||||||
|
|
||||||
|
Some newer Kasa devices require authentication. These are marked with <sup>*</sup> in the list below.
|
||||||
|
|
||||||
|
### Plugs
|
||||||
|
|
||||||
|
- **EP10**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||||
|
- **EP25**
|
||||||
|
- Hardware: 2.6 (US) / Firmware: 1.0.1<sup>\*</sup>
|
||||||
|
- Hardware: 2.6 (US) / Firmware: 1.0.2<sup>\*</sup>
|
||||||
|
- **HS100**
|
||||||
|
- Hardware: 1.0 (UK) / Firmware: 1.2.6
|
||||||
|
- Hardware: 4.1 (UK) / Firmware: 1.1.0<sup>\*</sup>
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.2.5
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.5.6
|
||||||
|
- **HS103**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.5.7
|
||||||
|
- Hardware: 2.1 (US) / Firmware: 1.1.2
|
||||||
|
- Hardware: 2.1 (US) / Firmware: 1.1.4
|
||||||
|
- **HS105**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.2.9
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.5.6
|
||||||
|
- **HS110**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.2.5
|
||||||
|
- Hardware: 2.0 (EU) / Firmware: 1.5.2
|
||||||
|
- Hardware: 4.0 (EU) / Firmware: 1.0.4
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.8
|
||||||
|
- **KP100**
|
||||||
|
- Hardware: 3.0 (US) / Firmware: 1.0.1
|
||||||
|
- **KP105**
|
||||||
|
- Hardware: 1.0 (UK) / Firmware: 1.0.5
|
||||||
|
- Hardware: 1.0 (UK) / Firmware: 1.0.7
|
||||||
|
- **KP115**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.0.16
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.17
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.21
|
||||||
|
- **KP125**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.6
|
||||||
|
- **KP125M**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.3<sup>\*</sup>
|
||||||
|
- **KP401**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.0
|
||||||
|
|
||||||
|
### Power Strips
|
||||||
|
|
||||||
|
- **EP40**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||||
|
- **HS107**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.8
|
||||||
|
- **HS300**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.10
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.12
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.3
|
||||||
|
- **KP200**
|
||||||
|
- Hardware: 3.0 (US) / Firmware: 1.0.3
|
||||||
|
- **KP303**
|
||||||
|
- Hardware: 1.0 (UK) / Firmware: 1.0.3
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.3
|
||||||
|
- **KP400**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.10
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.6
|
||||||
|
|
||||||
|
### Wall Switches
|
||||||
|
|
||||||
|
- **ES20M**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.8
|
||||||
|
- **HS200**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.0
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.5.7
|
||||||
|
- Hardware: 5.0 (US) / Firmware: 1.0.2
|
||||||
|
- **HS210**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.5.8
|
||||||
|
- **HS220**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.5.7
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.5.7
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.3
|
||||||
|
- **KP405**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.5
|
||||||
|
- **KS200M**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.8
|
||||||
|
- **KS205**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.2<sup>\*</sup>
|
||||||
|
- **KS220M**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.4
|
||||||
|
- **KS225**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.2<sup>\*</sup>
|
||||||
|
- **KS230**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.14
|
||||||
|
|
||||||
|
### Bulbs
|
||||||
|
|
||||||
|
- **KL110**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.8.11
|
||||||
|
- **KL120**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.8.6
|
||||||
|
- **KL125**
|
||||||
|
- Hardware: 1.20 (US) / Firmware: 1.0.5
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.7
|
||||||
|
- Hardware: 4.0 (US) / Firmware: 1.0.5
|
||||||
|
- **KL130**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.8.8
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.8.11
|
||||||
|
- **KL135**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.6
|
||||||
|
- **KL50**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.13
|
||||||
|
- **KL60**
|
||||||
|
- Hardware: 1.0 (UN) / Firmware: 1.1.4
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.13
|
||||||
|
- **LB100**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.4.3
|
||||||
|
- **LB110**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.8.11
|
||||||
|
- **LB120**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.0
|
||||||
|
- **LB130**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.6.0
|
||||||
|
|
||||||
|
### Light Strips
|
||||||
|
|
||||||
|
- **KL400L5**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.5
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.8
|
||||||
|
- **KL420L5**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||||
|
- **KL430**
|
||||||
|
- Hardware: 2.0 (UN) / Firmware: 1.0.8
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.10
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.11
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.8
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.0.9
|
||||||
|
|
||||||
|
|
||||||
|
## Tapo devices
|
||||||
|
|
||||||
|
All Tapo devices require authentication.
|
||||||
|
|
||||||
|
### Plugs
|
||||||
|
|
||||||
|
- **P100**
|
||||||
|
- Hardware: 1.0.0 / Firmware: 1.1.3
|
||||||
|
- Hardware: 1.0.0 / Firmware: 1.3.7
|
||||||
|
- **P110**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.0.7
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.2.3
|
||||||
|
- Hardware: 1.0 (UK) / Firmware: 1.3.0
|
||||||
|
- **P125M**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.0
|
||||||
|
- **P135**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.5
|
||||||
|
- **TP15**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.3
|
||||||
|
|
||||||
|
### Power Strips
|
||||||
|
|
||||||
|
- **P300**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.0.13
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.0.7
|
||||||
|
- **TP25**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||||
|
|
||||||
|
### Wall Switches
|
||||||
|
|
||||||
|
- **S500D**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.5
|
||||||
|
- **S505**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.2
|
||||||
|
|
||||||
|
### Bulbs
|
||||||
|
|
||||||
|
- **L510B**
|
||||||
|
- Hardware: 3.0 (EU) / Firmware: 1.0.5
|
||||||
|
- **L510E**
|
||||||
|
- Hardware: 3.0 (US) / Firmware: 1.0.5
|
||||||
|
- Hardware: 3.0 (US) / Firmware: 1.1.2
|
||||||
|
- **L530E**
|
||||||
|
- Hardware: 3.0 (EU) / Firmware: 1.0.6
|
||||||
|
- Hardware: 3.0 (EU) / Firmware: 1.1.0
|
||||||
|
- Hardware: 3.0 (EU) / Firmware: 1.1.6
|
||||||
|
- Hardware: 2.0 (US) / Firmware: 1.1.0
|
||||||
|
|
||||||
|
### Light Strips
|
||||||
|
|
||||||
|
- **L900-10**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.0.17
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.0.11
|
||||||
|
- **L900-5**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.0.17
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.1.0
|
||||||
|
- **L920-5**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.0
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.3
|
||||||
|
- **L930-5**
|
||||||
|
- Hardware: 1.0 (US) / Firmware: 1.1.2
|
||||||
|
|
||||||
|
### Hubs
|
||||||
|
|
||||||
|
- **H100**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.2.3
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.5.5
|
||||||
|
|
||||||
|
|
||||||
|
<!--SUPPORTED_END-->
|
@ -1,43 +0,0 @@
|
|||||||
"""Script that checks if README.md is missing devices that have fixtures."""
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from kasa.tests.conftest import (
|
|
||||||
ALL_DEVICES,
|
|
||||||
BULBS,
|
|
||||||
DIMMERS,
|
|
||||||
LIGHT_STRIPS,
|
|
||||||
PLUGS,
|
|
||||||
STRIPS,
|
|
||||||
)
|
|
||||||
|
|
||||||
with open("README.md") as f:
|
|
||||||
readme = f.read()
|
|
||||||
|
|
||||||
typemap = {
|
|
||||||
"light strips": LIGHT_STRIPS,
|
|
||||||
"bulbs": BULBS,
|
|
||||||
"plugs": PLUGS,
|
|
||||||
"strips": STRIPS,
|
|
||||||
"dimmers": DIMMERS,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_device_type(dev, typemap):
|
|
||||||
for typename, devs in typemap.items():
|
|
||||||
if dev in devs:
|
|
||||||
return typename
|
|
||||||
else:
|
|
||||||
return "Unknown type"
|
|
||||||
|
|
||||||
|
|
||||||
found_unlisted = False
|
|
||||||
for dev in ALL_DEVICES:
|
|
||||||
regex = rf"^\*.*\s{dev}"
|
|
||||||
match = re.search(regex, readme, re.MULTILINE)
|
|
||||||
if match is None:
|
|
||||||
print(f"{dev} not listed in {_get_device_type(dev, typemap)}")
|
|
||||||
found_unlisted = True
|
|
||||||
|
|
||||||
if found_unlisted:
|
|
||||||
sys.exit(-1)
|
|
241
devtools/generate_supported.py
Executable file
241
devtools/generate_supported.py
Executable file
@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Script that checks supported devices and updates README.md and SUPPORTED.md."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from string import Template
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from kasa.device_factory import _get_device_type_from_sys_info
|
||||||
|
from kasa.device_type import DeviceType
|
||||||
|
from kasa.smart.smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
|
class SupportedVersion(NamedTuple):
|
||||||
|
"""Supported version."""
|
||||||
|
|
||||||
|
region: str
|
||||||
|
hw: str
|
||||||
|
fw: str
|
||||||
|
auth: bool
|
||||||
|
|
||||||
|
|
||||||
|
# The order of devices in this dict drives the display order
|
||||||
|
DEVICE_TYPE_TO_PRODUCT_GROUP = {
|
||||||
|
DeviceType.Plug: "Plugs",
|
||||||
|
DeviceType.Strip: "Power Strips",
|
||||||
|
DeviceType.StripSocket: "Power Strips",
|
||||||
|
DeviceType.Dimmer: "Wall Switches",
|
||||||
|
DeviceType.WallSwitch: "Wall Switches",
|
||||||
|
DeviceType.Bulb: "Bulbs",
|
||||||
|
DeviceType.LightStrip: "Light Strips",
|
||||||
|
DeviceType.Hub: "Hubs",
|
||||||
|
DeviceType.Sensor: "Sensors",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORTED_FILENAME = "SUPPORTED.md"
|
||||||
|
README_FILENAME = "README.md"
|
||||||
|
|
||||||
|
IOT_FOLDER = "kasa/tests/fixtures/"
|
||||||
|
SMART_FOLDER = "kasa/tests/fixtures/smart/"
|
||||||
|
|
||||||
|
|
||||||
|
def generate_supported(args):
|
||||||
|
"""Generate the SUPPORTED.md from the fixtures."""
|
||||||
|
print_diffs = "--print-diffs" in args
|
||||||
|
running_in_ci = "CI" in os.environ
|
||||||
|
print("Generating supported devices")
|
||||||
|
if running_in_ci:
|
||||||
|
print_diffs = True
|
||||||
|
print("Detected running in CI")
|
||||||
|
|
||||||
|
supported = {"kasa": {}, "tapo": {}}
|
||||||
|
|
||||||
|
_get_iot_supported(supported)
|
||||||
|
_get_smart_supported(supported)
|
||||||
|
|
||||||
|
readme_updated = _update_supported_file(
|
||||||
|
README_FILENAME, _supported_summary(supported), print_diffs
|
||||||
|
)
|
||||||
|
supported_updated = _update_supported_file(
|
||||||
|
SUPPORTED_FILENAME, _supported_detail(supported), print_diffs
|
||||||
|
)
|
||||||
|
if not readme_updated and not supported_updated:
|
||||||
|
print("Supported devices unchanged.")
|
||||||
|
|
||||||
|
|
||||||
|
def _update_supported_file(filename, supported_text, print_diffs) -> bool:
|
||||||
|
with open(filename) as f:
|
||||||
|
contents = f.readlines()
|
||||||
|
|
||||||
|
start_index = end_index = None
|
||||||
|
for index, line in enumerate(contents):
|
||||||
|
if line == "<!--SUPPORTED_START-->\n":
|
||||||
|
start_index = index + 1
|
||||||
|
if line == "<!--SUPPORTED_END-->\n":
|
||||||
|
end_index = index
|
||||||
|
|
||||||
|
current_text = "".join(contents[start_index:end_index])
|
||||||
|
if current_text != supported_text:
|
||||||
|
print(
|
||||||
|
f"{filename} has been modified with updated "
|
||||||
|
+ "supported devices, add file to commit."
|
||||||
|
)
|
||||||
|
if print_diffs:
|
||||||
|
print("##CURRENT##")
|
||||||
|
print(current_text)
|
||||||
|
print("##NEW##")
|
||||||
|
print(supported_text)
|
||||||
|
|
||||||
|
new_contents = contents[:start_index]
|
||||||
|
end_contents = contents[end_index:]
|
||||||
|
new_contents.append(supported_text)
|
||||||
|
new_contents.extend(end_contents)
|
||||||
|
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
new_contents_text = "".join(new_contents)
|
||||||
|
f.write(new_contents_text)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _supported_summary(supported):
|
||||||
|
return _supported_text(
|
||||||
|
supported,
|
||||||
|
"### Supported $brand$auth devices\n\n$types\n",
|
||||||
|
"- **$type_**: $models\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _supported_detail(supported):
|
||||||
|
return _supported_text(
|
||||||
|
supported,
|
||||||
|
"## $brand devices\n\n$preamble\n\n$types\n",
|
||||||
|
"### $type_\n\n$models\n",
|
||||||
|
"- **$model**\n$versions",
|
||||||
|
" - Hardware: $hw$region / Firmware: $fw$auth_flag\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _supported_text(
|
||||||
|
supported, brand_template, types_template, model_template="", version_template=""
|
||||||
|
):
|
||||||
|
brandt = Template(brand_template)
|
||||||
|
typest = Template(types_template)
|
||||||
|
modelt = Template(model_template)
|
||||||
|
versst = Template(version_template)
|
||||||
|
brands = ""
|
||||||
|
version: SupportedVersion
|
||||||
|
for brand, types in supported.items():
|
||||||
|
preamble_text = (
|
||||||
|
"Some newer Kasa devices require authentication. "
|
||||||
|
+ "These are marked with <sup>*</sup> in the list below."
|
||||||
|
if brand == "kasa"
|
||||||
|
else "All Tapo devices require authentication."
|
||||||
|
)
|
||||||
|
brand_text = brand.capitalize()
|
||||||
|
brand_auth = r"<sup>\*</sup>" if brand == "tapo" else ""
|
||||||
|
types_text = ""
|
||||||
|
for supported_type, models in sorted(
|
||||||
|
# Sort by device type order in the enum
|
||||||
|
types.items(),
|
||||||
|
key=lambda st: list(DEVICE_TYPE_TO_PRODUCT_GROUP.values()).index(st[0]),
|
||||||
|
):
|
||||||
|
models_list = []
|
||||||
|
models_text = ""
|
||||||
|
for model, versions in sorted(models.items()):
|
||||||
|
auth_count = 0
|
||||||
|
versions_text = ""
|
||||||
|
for version in sorted(versions):
|
||||||
|
region_text = f" ({version.region})" if version.region else ""
|
||||||
|
auth_count += 1 if version.auth else 0
|
||||||
|
vauth_flag = (
|
||||||
|
r"<sup>\*</sup>" if version.auth and brand == "kasa" else ""
|
||||||
|
)
|
||||||
|
if version_template:
|
||||||
|
versions_text += versst.substitute(
|
||||||
|
hw=version.hw,
|
||||||
|
fw=version.fw,
|
||||||
|
region=region_text,
|
||||||
|
auth_flag=vauth_flag,
|
||||||
|
)
|
||||||
|
if brand == "kasa" and auth_count > 0:
|
||||||
|
auth_flag = (
|
||||||
|
r"<sup>\*</sup>"
|
||||||
|
if auth_count == len(versions)
|
||||||
|
else r"<sup>\*\*</sup>"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
auth_flag = ""
|
||||||
|
if model_template:
|
||||||
|
models_text += modelt.substitute(
|
||||||
|
model=model, versions=versions_text, auth_flag=auth_flag
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
models_list.append(f"{model}{auth_flag}")
|
||||||
|
models_text = models_text if models_text else ", ".join(models_list)
|
||||||
|
types_text += typest.substitute(type_=supported_type, models=models_text)
|
||||||
|
brands += brandt.substitute(
|
||||||
|
brand=brand_text, types=types_text, auth=brand_auth, preamble=preamble_text
|
||||||
|
)
|
||||||
|
return brands
|
||||||
|
|
||||||
|
|
||||||
|
def _get_smart_supported(supported):
|
||||||
|
for file in Path(SMART_FOLDER).glob("*.json"):
|
||||||
|
with file.open() as f:
|
||||||
|
fixture_data = json.load(f)
|
||||||
|
|
||||||
|
model, _, region = fixture_data["discovery_result"]["device_model"].partition(
|
||||||
|
"("
|
||||||
|
)
|
||||||
|
# P100 doesn't have region HW
|
||||||
|
region = region.replace(")", "") if region else ""
|
||||||
|
device_type = fixture_data["discovery_result"]["device_type"]
|
||||||
|
_protocol, devicetype = device_type.split(".")
|
||||||
|
brand = devicetype[:4].lower()
|
||||||
|
components = [
|
||||||
|
component["id"]
|
||||||
|
for component in fixture_data["component_nego"]["component_list"]
|
||||||
|
]
|
||||||
|
dt = SmartDevice._get_device_type_from_components(components, device_type)
|
||||||
|
supported_type = DEVICE_TYPE_TO_PRODUCT_GROUP[dt]
|
||||||
|
|
||||||
|
hw_version = fixture_data["get_device_info"]["hw_ver"]
|
||||||
|
fw_version = fixture_data["get_device_info"]["fw_ver"]
|
||||||
|
fw_version = fw_version.split(" ", maxsplit=1)[0]
|
||||||
|
|
||||||
|
stype = supported[brand].setdefault(supported_type, {})
|
||||||
|
smodel = stype.setdefault(model, [])
|
||||||
|
smodel.append(
|
||||||
|
SupportedVersion(region=region, hw=hw_version, fw=fw_version, auth=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_iot_supported(supported):
|
||||||
|
for file in Path(IOT_FOLDER).glob("*.json"):
|
||||||
|
with file.open() as f:
|
||||||
|
fixture_data = json.load(f)
|
||||||
|
sysinfo = fixture_data["system"]["get_sysinfo"]
|
||||||
|
dt = _get_device_type_from_sys_info(fixture_data)
|
||||||
|
supported_type = DEVICE_TYPE_TO_PRODUCT_GROUP[dt]
|
||||||
|
|
||||||
|
model, _, region = sysinfo["model"][:-1].partition("(")
|
||||||
|
auth = "discovery_result" in fixture_data
|
||||||
|
stype = supported["kasa"].setdefault(supported_type, {})
|
||||||
|
smodel = stype.setdefault(model, [])
|
||||||
|
fw = sysinfo["sw_ver"].split(" ", maxsplit=1)[0]
|
||||||
|
smodel.append(
|
||||||
|
SupportedVersion(region=region, hw=sysinfo["hw_ver"], fw=fw, auth=auth)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point to module."""
|
||||||
|
generate_supported(sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
generate_supported(sys.argv[1:])
|
3
docs/source/SUPPORTED.md
Normal file
3
docs/source/SUPPORTED.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
```{include} ../../SUPPORTED.md
|
||||||
|
:relative-docs: doc/source
|
||||||
|
```
|
@ -15,3 +15,4 @@
|
|||||||
smartdimmer
|
smartdimmer
|
||||||
smartstrip
|
smartstrip
|
||||||
smartlightstrip
|
smartlightstrip
|
||||||
|
SUPPORTED
|
||||||
|
12
kasa/cli.py
12
kasa/cli.py
@ -26,7 +26,15 @@ from kasa import (
|
|||||||
UnsupportedDeviceError,
|
UnsupportedDeviceError,
|
||||||
)
|
)
|
||||||
from kasa.discover import DiscoveryResult
|
from kasa.discover import DiscoveryResult
|
||||||
from kasa.iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip
|
from kasa.iot import (
|
||||||
|
IotBulb,
|
||||||
|
IotDevice,
|
||||||
|
IotDimmer,
|
||||||
|
IotLightStrip,
|
||||||
|
IotPlug,
|
||||||
|
IotStrip,
|
||||||
|
IotWallSwitch,
|
||||||
|
)
|
||||||
from kasa.smart import SmartBulb, SmartDevice
|
from kasa.smart import SmartBulb, SmartDevice
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -63,11 +71,13 @@ echo = _do_echo
|
|||||||
|
|
||||||
TYPE_TO_CLASS = {
|
TYPE_TO_CLASS = {
|
||||||
"plug": IotPlug,
|
"plug": IotPlug,
|
||||||
|
"switch": IotWallSwitch,
|
||||||
"bulb": IotBulb,
|
"bulb": IotBulb,
|
||||||
"dimmer": IotDimmer,
|
"dimmer": IotDimmer,
|
||||||
"strip": IotStrip,
|
"strip": IotStrip,
|
||||||
"lightstrip": IotLightStrip,
|
"lightstrip": IotLightStrip,
|
||||||
"iot.plug": IotPlug,
|
"iot.plug": IotPlug,
|
||||||
|
"iot.switch": IotWallSwitch,
|
||||||
"iot.bulb": IotBulb,
|
"iot.bulb": IotBulb,
|
||||||
"iot.dimmer": IotDimmer,
|
"iot.dimmer": IotDimmer,
|
||||||
"iot.strip": IotStrip,
|
"iot.strip": IotStrip,
|
||||||
|
@ -212,6 +212,11 @@ class Device(ABC):
|
|||||||
"""Return True if the device is a plug."""
|
"""Return True if the device is a plug."""
|
||||||
return self.device_type == DeviceType.Plug
|
return self.device_type == DeviceType.Plug
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_wallswitch(self) -> bool:
|
||||||
|
"""Return True if the device is a switch."""
|
||||||
|
return self.device_type == DeviceType.WallSwitch
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_strip(self) -> bool:
|
def is_strip(self) -> bool:
|
||||||
"""Return True if the device is a strip."""
|
"""Return True if the device is a strip."""
|
||||||
|
@ -5,9 +5,18 @@ from typing import Any, Dict, Optional, Tuple, Type
|
|||||||
|
|
||||||
from .aestransport import AesTransport
|
from .aestransport import AesTransport
|
||||||
from .device import Device
|
from .device import Device
|
||||||
|
from .device_type import DeviceType
|
||||||
from .deviceconfig import DeviceConfig
|
from .deviceconfig import DeviceConfig
|
||||||
from .exceptions import KasaException, UnsupportedDeviceError
|
from .exceptions import KasaException, UnsupportedDeviceError
|
||||||
from .iot import IotBulb, IotDevice, IotDimmer, IotLightStrip, IotPlug, IotStrip
|
from .iot import (
|
||||||
|
IotBulb,
|
||||||
|
IotDevice,
|
||||||
|
IotDimmer,
|
||||||
|
IotLightStrip,
|
||||||
|
IotPlug,
|
||||||
|
IotStrip,
|
||||||
|
IotWallSwitch,
|
||||||
|
)
|
||||||
from .iotprotocol import IotProtocol
|
from .iotprotocol import IotProtocol
|
||||||
from .klaptransport import KlapTransport, KlapTransportV2
|
from .klaptransport import KlapTransport, KlapTransportV2
|
||||||
from .protocol import (
|
from .protocol import (
|
||||||
@ -105,7 +114,7 @@ async def _connect(config: DeviceConfig, protocol: BaseProtocol) -> "Device":
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_device_class_from_sys_info(info: Dict[str, Any]) -> Type[IotDevice]:
|
def _get_device_type_from_sys_info(info: Dict[str, Any]) -> DeviceType:
|
||||||
"""Find SmartDevice subclass for device described by passed data."""
|
"""Find SmartDevice subclass for device described by passed data."""
|
||||||
if "system" not in info or "get_sysinfo" not in info["system"]:
|
if "system" not in info or "get_sysinfo" not in info["system"]:
|
||||||
raise KasaException("No 'system' or 'get_sysinfo' in response")
|
raise KasaException("No 'system' or 'get_sysinfo' in response")
|
||||||
@ -116,22 +125,36 @@ def get_device_class_from_sys_info(info: Dict[str, Any]) -> Type[IotDevice]:
|
|||||||
raise KasaException("Unable to find the device type field!")
|
raise KasaException("Unable to find the device type field!")
|
||||||
|
|
||||||
if "dev_name" in sysinfo and "Dimmer" in sysinfo["dev_name"]:
|
if "dev_name" in sysinfo and "Dimmer" in sysinfo["dev_name"]:
|
||||||
return IotDimmer
|
return DeviceType.Dimmer
|
||||||
|
|
||||||
if "smartplug" in type_.lower():
|
if "smartplug" in type_.lower():
|
||||||
if "children" in sysinfo:
|
if "children" in sysinfo:
|
||||||
return IotStrip
|
return DeviceType.Strip
|
||||||
|
if (dev_name := sysinfo.get("dev_name")) and "light" in dev_name.lower():
|
||||||
return IotPlug
|
return DeviceType.WallSwitch
|
||||||
|
return DeviceType.Plug
|
||||||
|
|
||||||
if "smartbulb" in type_.lower():
|
if "smartbulb" in type_.lower():
|
||||||
if "length" in sysinfo: # strips have length
|
if "length" in sysinfo: # strips have length
|
||||||
return IotLightStrip
|
return DeviceType.LightStrip
|
||||||
|
|
||||||
return IotBulb
|
return DeviceType.Bulb
|
||||||
raise UnsupportedDeviceError("Unknown device type: %s" % type_)
|
raise UnsupportedDeviceError("Unknown device type: %s" % type_)
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_class_from_sys_info(sysinfo: Dict[str, Any]) -> Type[IotDevice]:
|
||||||
|
"""Find SmartDevice subclass for device described by passed data."""
|
||||||
|
TYPE_TO_CLASS = {
|
||||||
|
DeviceType.Bulb: IotBulb,
|
||||||
|
DeviceType.Plug: IotPlug,
|
||||||
|
DeviceType.Dimmer: IotDimmer,
|
||||||
|
DeviceType.Strip: IotStrip,
|
||||||
|
DeviceType.WallSwitch: IotWallSwitch,
|
||||||
|
DeviceType.LightStrip: IotLightStrip,
|
||||||
|
}
|
||||||
|
return TYPE_TO_CLASS[_get_device_type_from_sys_info(sysinfo)]
|
||||||
|
|
||||||
|
|
||||||
def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]:
|
def get_device_class_from_family(device_type: str) -> Optional[Type[Device]]:
|
||||||
"""Return the device class from the type name."""
|
"""Return the device class from the type name."""
|
||||||
supported_device_types: Dict[str, Type[Device]] = {
|
supported_device_types: Dict[str, Type[Device]] = {
|
||||||
|
@ -11,6 +11,7 @@ class DeviceType(Enum):
|
|||||||
Plug = "plug"
|
Plug = "plug"
|
||||||
Bulb = "bulb"
|
Bulb = "bulb"
|
||||||
Strip = "strip"
|
Strip = "strip"
|
||||||
|
WallSwitch = "wallswitch"
|
||||||
StripSocket = "stripsocket"
|
StripSocket = "stripsocket"
|
||||||
Dimmer = "dimmer"
|
Dimmer = "dimmer"
|
||||||
LightStrip = "lightstrip"
|
LightStrip = "lightstrip"
|
||||||
|
@ -3,7 +3,7 @@ from .iotbulb import IotBulb
|
|||||||
from .iotdevice import IotDevice
|
from .iotdevice import IotDevice
|
||||||
from .iotdimmer import IotDimmer
|
from .iotdimmer import IotDimmer
|
||||||
from .iotlightstrip import IotLightStrip
|
from .iotlightstrip import IotLightStrip
|
||||||
from .iotplug import IotPlug
|
from .iotplug import IotPlug, IotWallSwitch
|
||||||
from .iotstrip import IotStrip
|
from .iotstrip import IotStrip
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@ -13,4 +13,5 @@ __all__ = [
|
|||||||
"IotStrip",
|
"IotStrip",
|
||||||
"IotDimmer",
|
"IotDimmer",
|
||||||
"IotLightStrip",
|
"IotLightStrip",
|
||||||
|
"IotWallSwitch",
|
||||||
]
|
]
|
||||||
|
@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class IotPlug(IotDevice):
|
class IotPlug(IotDevice):
|
||||||
r"""Representation of a TP-Link Smart Switch.
|
r"""Representation of a TP-Link Smart Plug.
|
||||||
|
|
||||||
To initialize, you have to await :func:`update()` at least once.
|
To initialize, you have to await :func:`update()` at least once.
|
||||||
This will allow accessing the properties using the exposed properties.
|
This will allow accessing the properties using the exposed properties.
|
||||||
@ -101,3 +101,17 @@ class IotPlug(IotDevice):
|
|||||||
def state_information(self) -> Dict[str, Any]:
|
def state_information(self) -> Dict[str, Any]:
|
||||||
"""Return switch-specific state information."""
|
"""Return switch-specific state information."""
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class IotWallSwitch(IotPlug):
|
||||||
|
"""Representation of a TP-Link Smart Wall Switch."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
host: str,
|
||||||
|
*,
|
||||||
|
config: Optional[DeviceConfig] = None,
|
||||||
|
protocol: Optional[BaseProtocol] = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(host=host, config=config, protocol=protocol)
|
||||||
|
self._device_type = DeviceType.WallSwitch
|
||||||
|
@ -63,12 +63,6 @@ class SmartDevice(Device):
|
|||||||
)
|
)
|
||||||
for child_info in children
|
for child_info in children
|
||||||
}
|
}
|
||||||
# TODO: This may not be the best approach, but it allows distinguishing
|
|
||||||
# between power strips and hubs for the time being.
|
|
||||||
if all(child.is_plug for child in self._children.values()):
|
|
||||||
self._device_type = DeviceType.Strip
|
|
||||||
else:
|
|
||||||
self._device_type = DeviceType.Hub
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self) -> Sequence["SmartDevice"]:
|
def children(self) -> Sequence["SmartDevice"]:
|
||||||
@ -519,21 +513,30 @@ class SmartDevice(Device):
|
|||||||
if self._device_type is not DeviceType.Unknown:
|
if self._device_type is not DeviceType.Unknown:
|
||||||
return self._device_type
|
return self._device_type
|
||||||
|
|
||||||
if self.children:
|
self._device_type = self._get_device_type_from_components(
|
||||||
if "SMART.TAPOHUB" in self.sys_info["type"]:
|
list(self._components.keys()), self._info["type"]
|
||||||
self._device_type = DeviceType.Hub
|
)
|
||||||
else:
|
|
||||||
self._device_type = DeviceType.Strip
|
|
||||||
elif "light_strip" in self._components:
|
|
||||||
self._device_type = DeviceType.LightStrip
|
|
||||||
elif "dimmer_calibration" in self._components:
|
|
||||||
self._device_type = DeviceType.Dimmer
|
|
||||||
elif "brightness" in self._components:
|
|
||||||
self._device_type = DeviceType.Bulb
|
|
||||||
elif "PLUG" in self.sys_info["type"]:
|
|
||||||
self._device_type = DeviceType.Plug
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("Unknown device type, falling back to plug")
|
|
||||||
self._device_type = DeviceType.Plug
|
|
||||||
|
|
||||||
return self._device_type
|
return self._device_type
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_device_type_from_components(
|
||||||
|
components: List[str], device_type: str
|
||||||
|
) -> DeviceType:
|
||||||
|
"""Find type to be displayed as a supported device category."""
|
||||||
|
if "HUB" in device_type:
|
||||||
|
return DeviceType.Hub
|
||||||
|
if "PLUG" in device_type:
|
||||||
|
if "child_device" in components:
|
||||||
|
return DeviceType.Strip
|
||||||
|
return DeviceType.Plug
|
||||||
|
if "light_strip" in components:
|
||||||
|
return DeviceType.LightStrip
|
||||||
|
if "dimmer_calibration" in components:
|
||||||
|
return DeviceType.Dimmer
|
||||||
|
if "brightness" in components:
|
||||||
|
return DeviceType.Bulb
|
||||||
|
if "SWITCH" in device_type:
|
||||||
|
return DeviceType.WallSwitch
|
||||||
|
_LOGGER.warning("Unknown device type, falling back to plug")
|
||||||
|
return DeviceType.Plug
|
||||||
|
@ -7,7 +7,7 @@ from kasa import (
|
|||||||
Device,
|
Device,
|
||||||
Discover,
|
Discover,
|
||||||
)
|
)
|
||||||
from kasa.iot import IotBulb, IotDimmer, IotLightStrip, IotPlug, IotStrip
|
from kasa.iot import IotBulb, IotDimmer, IotLightStrip, IotPlug, IotStrip, IotWallSwitch
|
||||||
from kasa.smart import SmartBulb, SmartDevice
|
from kasa.smart import SmartBulb, SmartDevice
|
||||||
|
|
||||||
from .fakeprotocol_iot import FakeIotProtocol
|
from .fakeprotocol_iot import FakeIotProtocol
|
||||||
@ -60,15 +60,12 @@ PLUGS_IOT = {
|
|||||||
"HS103",
|
"HS103",
|
||||||
"HS105",
|
"HS105",
|
||||||
"HS110",
|
"HS110",
|
||||||
"HS200",
|
|
||||||
"HS210",
|
|
||||||
"EP10",
|
"EP10",
|
||||||
"KP100",
|
"KP100",
|
||||||
"KP105",
|
"KP105",
|
||||||
"KP115",
|
"KP115",
|
||||||
"KP125",
|
"KP125",
|
||||||
"KP401",
|
"KP401",
|
||||||
"KS200M",
|
|
||||||
}
|
}
|
||||||
# P135 supports dimming, but its not currently support
|
# P135 supports dimming, but its not currently support
|
||||||
# by the library
|
# by the library
|
||||||
@ -77,15 +74,25 @@ PLUGS_SMART = {
|
|||||||
"P110",
|
"P110",
|
||||||
"KP125M",
|
"KP125M",
|
||||||
"EP25",
|
"EP25",
|
||||||
"KS205",
|
|
||||||
"P125M",
|
"P125M",
|
||||||
"S505",
|
|
||||||
"TP15",
|
"TP15",
|
||||||
}
|
}
|
||||||
PLUGS = {
|
PLUGS = {
|
||||||
*PLUGS_IOT,
|
*PLUGS_IOT,
|
||||||
*PLUGS_SMART,
|
*PLUGS_SMART,
|
||||||
}
|
}
|
||||||
|
SWITCHES_IOT = {
|
||||||
|
"HS200",
|
||||||
|
"HS210",
|
||||||
|
"KS200M",
|
||||||
|
}
|
||||||
|
SWITCHES_SMART = {
|
||||||
|
"KS205",
|
||||||
|
"KS225",
|
||||||
|
"S500D",
|
||||||
|
"S505",
|
||||||
|
}
|
||||||
|
SWITCHES = {*SWITCHES_IOT, *SWITCHES_SMART}
|
||||||
STRIPS_IOT = {"HS107", "HS300", "KP303", "KP200", "KP400", "EP40"}
|
STRIPS_IOT = {"HS107", "HS300", "KP303", "KP200", "KP400", "EP40"}
|
||||||
STRIPS_SMART = {"P300", "TP25"}
|
STRIPS_SMART = {"P300", "TP25"}
|
||||||
STRIPS = {*STRIPS_IOT, *STRIPS_SMART}
|
STRIPS = {*STRIPS_IOT, *STRIPS_SMART}
|
||||||
@ -105,12 +112,15 @@ WITH_EMETER = {*WITH_EMETER_IOT, *WITH_EMETER_SMART}
|
|||||||
|
|
||||||
DIMMABLE = {*BULBS, *DIMMERS}
|
DIMMABLE = {*BULBS, *DIMMERS}
|
||||||
|
|
||||||
ALL_DEVICES_IOT = BULBS_IOT.union(PLUGS_IOT).union(STRIPS_IOT).union(DIMMERS_IOT)
|
ALL_DEVICES_IOT = (
|
||||||
|
BULBS_IOT.union(PLUGS_IOT).union(STRIPS_IOT).union(DIMMERS_IOT).union(SWITCHES_IOT)
|
||||||
|
)
|
||||||
ALL_DEVICES_SMART = (
|
ALL_DEVICES_SMART = (
|
||||||
BULBS_SMART.union(PLUGS_SMART)
|
BULBS_SMART.union(PLUGS_SMART)
|
||||||
.union(STRIPS_SMART)
|
.union(STRIPS_SMART)
|
||||||
.union(DIMMERS_SMART)
|
.union(DIMMERS_SMART)
|
||||||
.union(HUBS_SMART)
|
.union(HUBS_SMART)
|
||||||
|
.union(SWITCHES_SMART)
|
||||||
)
|
)
|
||||||
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
ALL_DEVICES = ALL_DEVICES_IOT.union(ALL_DEVICES_SMART)
|
||||||
|
|
||||||
@ -160,7 +170,14 @@ no_emeter_iot = parametrize(
|
|||||||
)
|
)
|
||||||
|
|
||||||
bulb = parametrize("bulbs", model_filter=BULBS, protocol_filter={"SMART", "IOT"})
|
bulb = parametrize("bulbs", model_filter=BULBS, protocol_filter={"SMART", "IOT"})
|
||||||
plug = parametrize("plugs", model_filter=PLUGS, protocol_filter={"IOT"})
|
plug = parametrize("plugs", model_filter=PLUGS, protocol_filter={"IOT", "SMART"})
|
||||||
|
plug_iot = parametrize("plugs iot", model_filter=PLUGS, protocol_filter={"IOT"})
|
||||||
|
wallswitch = parametrize(
|
||||||
|
"wall switches", model_filter=SWITCHES, protocol_filter={"IOT", "SMART"}
|
||||||
|
)
|
||||||
|
wallswitch_iot = parametrize(
|
||||||
|
"wall switches iot", model_filter=SWITCHES, protocol_filter={"IOT"}
|
||||||
|
)
|
||||||
strip = parametrize("strips", model_filter=STRIPS, protocol_filter={"SMART", "IOT"})
|
strip = parametrize("strips", model_filter=STRIPS, protocol_filter={"SMART", "IOT"})
|
||||||
dimmer = parametrize("dimmers", model_filter=DIMMERS, protocol_filter={"IOT"})
|
dimmer = parametrize("dimmers", model_filter=DIMMERS, protocol_filter={"IOT"})
|
||||||
lightstrip = parametrize(
|
lightstrip = parametrize(
|
||||||
@ -213,6 +230,9 @@ strip_smart = parametrize(
|
|||||||
plug_smart = parametrize(
|
plug_smart = parametrize(
|
||||||
"plug devices smart", model_filter=PLUGS_SMART, protocol_filter={"SMART"}
|
"plug devices smart", model_filter=PLUGS_SMART, protocol_filter={"SMART"}
|
||||||
)
|
)
|
||||||
|
switch_smart = parametrize(
|
||||||
|
"switch devices smart", model_filter=SWITCHES_SMART, protocol_filter={"SMART"}
|
||||||
|
)
|
||||||
bulb_smart = parametrize(
|
bulb_smart = parametrize(
|
||||||
"bulb devices smart", model_filter=BULBS_SMART, protocol_filter={"SMART"}
|
"bulb devices smart", model_filter=BULBS_SMART, protocol_filter={"SMART"}
|
||||||
)
|
)
|
||||||
@ -239,8 +259,8 @@ def check_categories():
|
|||||||
+ strip.args[1]
|
+ strip.args[1]
|
||||||
+ plug.args[1]
|
+ plug.args[1]
|
||||||
+ bulb.args[1]
|
+ bulb.args[1]
|
||||||
|
+ wallswitch.args[1]
|
||||||
+ lightstrip.args[1]
|
+ lightstrip.args[1]
|
||||||
+ plug_smart.args[1]
|
|
||||||
+ bulb_smart.args[1]
|
+ bulb_smart.args[1]
|
||||||
+ dimmers_smart.args[1]
|
+ dimmers_smart.args[1]
|
||||||
+ hubs_smart.args[1]
|
+ hubs_smart.args[1]
|
||||||
@ -263,6 +283,9 @@ def device_for_fixture_name(model, protocol):
|
|||||||
for d in PLUGS_SMART:
|
for d in PLUGS_SMART:
|
||||||
if d in model:
|
if d in model:
|
||||||
return SmartDevice
|
return SmartDevice
|
||||||
|
for d in SWITCHES_SMART:
|
||||||
|
if d in model:
|
||||||
|
return SmartDevice
|
||||||
for d in BULBS_SMART:
|
for d in BULBS_SMART:
|
||||||
if d in model:
|
if d in model:
|
||||||
return SmartBulb
|
return SmartBulb
|
||||||
@ -283,6 +306,9 @@ def device_for_fixture_name(model, protocol):
|
|||||||
for d in PLUGS_IOT:
|
for d in PLUGS_IOT:
|
||||||
if d in model:
|
if d in model:
|
||||||
return IotPlug
|
return IotPlug
|
||||||
|
for d in SWITCHES_IOT:
|
||||||
|
if d in model:
|
||||||
|
return IotWallSwitch
|
||||||
|
|
||||||
# Light strips are recognized also as bulbs, so this has to go first
|
# Light strips are recognized also as bulbs, so this has to go first
|
||||||
for d in BULBS_IOT_LIGHT_STRIP:
|
for d in BULBS_IOT_LIGHT_STRIP:
|
||||||
@ -325,6 +351,13 @@ async def get_device_for_fixture(fixture_data: FixtureInfo):
|
|||||||
d.protocol = FakeSmartProtocol(fixture_data.data, fixture_data.name)
|
d.protocol = FakeSmartProtocol(fixture_data.data, fixture_data.name)
|
||||||
else:
|
else:
|
||||||
d.protocol = FakeIotProtocol(fixture_data.data)
|
d.protocol = FakeIotProtocol(fixture_data.data)
|
||||||
|
if "discovery_result" in fixture_data.data:
|
||||||
|
discovery_data = {"result": fixture_data.data["discovery_result"]}
|
||||||
|
else:
|
||||||
|
discovery_data = {
|
||||||
|
"system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]}
|
||||||
|
}
|
||||||
|
d.update_from_discover_info(discovery_data)
|
||||||
await _update_and_close(d)
|
await _update_and_close(d)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -10,7 +10,11 @@ from kasa import (
|
|||||||
Discover,
|
Discover,
|
||||||
KasaException,
|
KasaException,
|
||||||
)
|
)
|
||||||
from kasa.device_factory import connect, get_protocol
|
from kasa.device_factory import (
|
||||||
|
_get_device_type_from_sys_info,
|
||||||
|
connect,
|
||||||
|
get_protocol,
|
||||||
|
)
|
||||||
from kasa.deviceconfig import (
|
from kasa.deviceconfig import (
|
||||||
ConnectionType,
|
ConnectionType,
|
||||||
DeviceConfig,
|
DeviceConfig,
|
||||||
@ -18,6 +22,7 @@ from kasa.deviceconfig import (
|
|||||||
EncryptType,
|
EncryptType,
|
||||||
)
|
)
|
||||||
from kasa.discover import DiscoveryResult
|
from kasa.discover import DiscoveryResult
|
||||||
|
from kasa.smart.smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
def _get_connection_type_device_class(discovery_info):
|
def _get_connection_type_device_class(discovery_info):
|
||||||
@ -146,3 +151,16 @@ async def test_connect_http_client(discovery_data, mocker):
|
|||||||
assert dev.protocol._transport._http_client.client == http_client
|
assert dev.protocol._transport._http_client.client == http_client
|
||||||
await dev.disconnect()
|
await dev.disconnect()
|
||||||
await http_client.close()
|
await http_client.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_types(dev: Device):
|
||||||
|
await dev.update()
|
||||||
|
if isinstance(dev, SmartDevice):
|
||||||
|
device_type = dev._discovery_info["result"]["device_type"]
|
||||||
|
res = SmartDevice._get_device_type_from_components(
|
||||||
|
dev._components.keys(), device_type
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
res = _get_device_type_from_sys_info(dev._last_update)
|
||||||
|
|
||||||
|
assert dev.device_type == res
|
||||||
|
@ -29,8 +29,9 @@ from .conftest import (
|
|||||||
dimmer,
|
dimmer,
|
||||||
lightstrip,
|
lightstrip,
|
||||||
new_discovery,
|
new_discovery,
|
||||||
plug,
|
plug_iot,
|
||||||
strip_iot,
|
strip_iot,
|
||||||
|
wallswitch_iot,
|
||||||
)
|
)
|
||||||
|
|
||||||
UNSUPPORTED = {
|
UNSUPPORTED = {
|
||||||
@ -55,7 +56,14 @@ UNSUPPORTED = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@plug
|
@wallswitch_iot
|
||||||
|
async def test_type_detection_switch(dev: Device):
|
||||||
|
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||||
|
assert d.is_wallswitch
|
||||||
|
assert d.device_type == DeviceType.WallSwitch
|
||||||
|
|
||||||
|
|
||||||
|
@plug_iot
|
||||||
async def test_type_detection_plug(dev: Device):
|
async def test_type_detection_plug(dev: Device):
|
||||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||||
assert d.is_plug
|
assert d.is_plug
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from kasa import DeviceType
|
from kasa import DeviceType
|
||||||
|
|
||||||
from .conftest import plug, plug_smart
|
from .conftest import plug_iot, plug_smart, switch_smart, wallswitch_iot
|
||||||
from .test_smartdevice import SYSINFO_SCHEMA
|
from .test_smartdevice import SYSINFO_SCHEMA
|
||||||
|
|
||||||
# these schemas should go to the mainlib as
|
# these schemas should go to the mainlib as
|
||||||
@ -8,7 +8,7 @@ from .test_smartdevice import SYSINFO_SCHEMA
|
|||||||
# as well as to check that faked devices are operating properly.
|
# as well as to check that faked devices are operating properly.
|
||||||
|
|
||||||
|
|
||||||
@plug
|
@plug_iot
|
||||||
async def test_plug_sysinfo(dev):
|
async def test_plug_sysinfo(dev):
|
||||||
assert dev.sys_info is not None
|
assert dev.sys_info is not None
|
||||||
SYSINFO_SCHEMA(dev.sys_info)
|
SYSINFO_SCHEMA(dev.sys_info)
|
||||||
@ -19,8 +19,34 @@ async def test_plug_sysinfo(dev):
|
|||||||
assert dev.is_plug or dev.is_strip
|
assert dev.is_plug or dev.is_strip
|
||||||
|
|
||||||
|
|
||||||
@plug
|
@wallswitch_iot
|
||||||
async def test_led(dev):
|
async def test_switch_sysinfo(dev):
|
||||||
|
assert dev.sys_info is not None
|
||||||
|
SYSINFO_SCHEMA(dev.sys_info)
|
||||||
|
|
||||||
|
assert dev.model is not None
|
||||||
|
|
||||||
|
assert dev.device_type == DeviceType.WallSwitch
|
||||||
|
assert dev.is_wallswitch
|
||||||
|
|
||||||
|
|
||||||
|
@plug_iot
|
||||||
|
async def test_plug_led(dev):
|
||||||
|
original = dev.led
|
||||||
|
|
||||||
|
await dev.set_led(False)
|
||||||
|
await dev.update()
|
||||||
|
assert not dev.led
|
||||||
|
|
||||||
|
await dev.set_led(True)
|
||||||
|
await dev.update()
|
||||||
|
assert dev.led
|
||||||
|
|
||||||
|
await dev.set_led(original)
|
||||||
|
|
||||||
|
|
||||||
|
@wallswitch_iot
|
||||||
|
async def test_switch_led(dev):
|
||||||
original = dev.led
|
original = dev.led
|
||||||
|
|
||||||
await dev.set_led(False)
|
await dev.set_led(False)
|
||||||
@ -40,3 +66,13 @@ async def test_plug_device_info(dev):
|
|||||||
assert dev.model is not None
|
assert dev.model is not None
|
||||||
|
|
||||||
assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip
|
assert dev.device_type == DeviceType.Plug or dev.device_type == DeviceType.Strip
|
||||||
|
|
||||||
|
|
||||||
|
@switch_smart
|
||||||
|
async def test_switch_device_info(dev):
|
||||||
|
assert dev._info is not None
|
||||||
|
assert dev.model is not None
|
||||||
|
|
||||||
|
assert (
|
||||||
|
dev.device_type == DeviceType.WallSwitch or dev.device_type == DeviceType.Dimmer
|
||||||
|
)
|
||||||
|
30
poetry.lock
generated
30
poetry.lock
generated
@ -1726,20 +1726,22 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sphinx-rtd-theme"
|
name = "sphinx-rtd-theme"
|
||||||
version = "0.5.1"
|
version = "2.0.0"
|
||||||
description = "Read the Docs theme for Sphinx"
|
description = "Read the Docs theme for Sphinx"
|
||||||
optional = true
|
optional = true
|
||||||
python-versions = "*"
|
python-versions = ">=3.6"
|
||||||
files = [
|
files = [
|
||||||
{file = "sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl", hash = "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"},
|
{file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"},
|
||||||
{file = "sphinx_rtd_theme-0.5.1.tar.gz", hash = "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5"},
|
{file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
sphinx = "*"
|
docutils = "<0.21"
|
||||||
|
sphinx = ">=5,<8"
|
||||||
|
sphinxcontrib-jquery = ">=4,<5"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"]
|
dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sphinxcontrib-applehelp"
|
name = "sphinxcontrib-applehelp"
|
||||||
@ -1786,6 +1788,20 @@ files = [
|
|||||||
lint = ["docutils-stubs", "flake8", "mypy"]
|
lint = ["docutils-stubs", "flake8", "mypy"]
|
||||||
test = ["html5lib", "pytest"]
|
test = ["html5lib", "pytest"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sphinxcontrib-jquery"
|
||||||
|
version = "4.1"
|
||||||
|
description = "Extension to include jQuery on newer Sphinx releases"
|
||||||
|
optional = true
|
||||||
|
python-versions = ">=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"},
|
||||||
|
{file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Sphinx = ">=1.8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sphinxcontrib-jsmath"
|
name = "sphinxcontrib-jsmath"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -2130,4 +2146,4 @@ speedups = ["kasa-crypt", "orjson"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "aadbdc97219e5282f614f834c1318bbf8430fe769030f0a262e1922c5d7523b8"
|
content-hash = "fecc8870f967cc6da9d6e1fde0e9a9acd261d28c4ba57476250d17234dc2c876"
|
||||||
|
@ -35,7 +35,7 @@ kasa-crypt = { "version" = ">=0.2.0", optional = true }
|
|||||||
|
|
||||||
# required only for docs
|
# required only for docs
|
||||||
sphinx = { version = "^5", optional = true }
|
sphinx = { version = "^5", optional = true }
|
||||||
sphinx_rtd_theme = { version = "^0", optional = true }
|
sphinx_rtd_theme = { version = "^2", optional = true }
|
||||||
sphinxcontrib-programoutput = { version = "^0", optional = true }
|
sphinxcontrib-programoutput = { version = "^0", optional = true }
|
||||||
myst-parser = { version = "*", optional = true }
|
myst-parser = { version = "*", optional = true }
|
||||||
docutils = { version = ">=0.17", optional = true }
|
docutils = { version = ">=0.17", optional = true }
|
||||||
|
Loading…
Reference in New Issue
Block a user