mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-05-20 21:41:15 +00:00
Merge remote-tracking branch 'upstream/master' into feat/device_update
This commit is contained in:
commit
06c5e55ac6
@ -21,6 +21,11 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
additional_dependencies: [types-click]
|
additional_dependencies: [types-click]
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
kasa/modulemapping\.py|
|
||||||
|
)$
|
||||||
|
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/doc8
|
- repo: https://github.com/PyCQA/doc8
|
||||||
rev: 'v1.1.1'
|
rev: 'v1.1.1'
|
||||||
|
4
CONTRIBUTING.md
Normal file
4
CONTRIBUTING.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Contributing to python-kasa
|
||||||
|
|
||||||
|
All types of contributions are very welcome.
|
||||||
|
To make the process as straight-forward as possible, we have written [some instructions in our docs](https://python-miio.readthedocs.io/en/latest/contribute.html) to get you started.
|
38
README.md
38
README.md
@ -185,42 +185,12 @@ The device type specific documentation can be found in their separate pages:
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are very welcome! To simplify the process, we are leveraging automated checks and tests for contributions.
|
Contributions are very welcome! The easiest way to contribute is by [creating a fixture file](https://python-kasa.readthedocs.io/en/latest/contribute.html#contributing-fixture-files) for the automated test suite if your device hardware and firmware version is not currently listed as supported.
|
||||||
|
Please refer to [our contributing guidelines](https://python-kasa.readthedocs.io/en/latest/contribute.html).
|
||||||
### Setting up development environment
|
|
||||||
|
|
||||||
To get started, simply clone this repository and initialize the development environment.
|
|
||||||
We are using [poetry](https://python-poetry.org) for dependency management, so after cloning the repository simply execute
|
|
||||||
`poetry install` which will install all necessary packages and create a virtual environment for you.
|
|
||||||
|
|
||||||
### Code-style checks
|
|
||||||
|
|
||||||
We use several tools to automatically check all contributions. The simplest way to verify that everything is formatted properly
|
|
||||||
before creating a pull request, consider activating the pre-commit hooks by executing `pre-commit install`.
|
|
||||||
This will make sure that the checks are passing when you do a commit.
|
|
||||||
|
|
||||||
You can also execute the checks by running either `tox -e lint` to only do the linting checks, or `tox` to also execute the tests.
|
|
||||||
|
|
||||||
### Running tests
|
|
||||||
|
|
||||||
You can run tests on the library by executing `pytest` in the source directory.
|
|
||||||
This will run the tests against contributed example responses, but you can also execute the tests against a real device:
|
|
||||||
```
|
|
||||||
$ pytest --ip <address>
|
|
||||||
```
|
|
||||||
Note that this will perform state changes on the device.
|
|
||||||
|
|
||||||
### Analyzing network captures
|
|
||||||
|
|
||||||
The simplest way to add support for a new device or to improve existing ones is to capture traffic between the mobile app and the device.
|
|
||||||
After capturing the traffic, you can either use the [softScheck's wireshark dissector](https://github.com/softScheck/tplink-smartplug#wireshark-dissector)
|
|
||||||
or the `parse_pcap.py` script contained inside the `devtools` directory.
|
|
||||||
Note, that this works currently only on kasa-branded devices which use port 9999 for communications.
|
|
||||||
|
|
||||||
|
|
||||||
## Supported devices
|
## 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).
|
The following devices have been tested and confirmed as working. If your device is unlisted but working, please consider [contributing a fixture file](https://python-kasa.readthedocs.io/en/latest/contribute.html#contributing-fixture-files).
|
||||||
|
|
||||||
<!--Do not edit text inside the SUPPORTED section below -->
|
<!--Do not edit text inside the SUPPORTED section below -->
|
||||||
<!--SUPPORTED_START-->
|
<!--SUPPORTED_START-->
|
||||||
@ -242,7 +212,7 @@ The following devices have been tested and confirmed as working. If your device
|
|||||||
- **Bulbs**: L510B, L510E, L530E
|
- **Bulbs**: L510B, L510E, L530E
|
||||||
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
|
- **Light Strips**: L900-10, L900-5, L920-5, L930-5
|
||||||
- **Hubs**: H100
|
- **Hubs**: H100
|
||||||
- **Hub-Connected Devices<sup>\*\*\*</sup>**: T300, T310, T315
|
- **Hub-Connected Devices<sup>\*\*\*</sup>**: T110, T300, T310, T315
|
||||||
|
|
||||||
<!--SUPPORTED_END-->
|
<!--SUPPORTED_END-->
|
||||||
<sup>\*</sup> Model requires authentication<br>
|
<sup>\*</sup> Model requires authentication<br>
|
||||||
|
@ -137,6 +137,7 @@ Some newer Kasa devices require authentication. These are marked with <sup>*</su
|
|||||||
### Hub-Connected Devices
|
### Hub-Connected Devices
|
||||||
|
|
||||||
- **KE100**
|
- **KE100**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 2.4.0<sup>\*</sup>
|
||||||
- Hardware: 1.0 (EU) / Firmware: 2.8.0<sup>\*</sup>
|
- Hardware: 1.0 (EU) / Firmware: 2.8.0<sup>\*</sup>
|
||||||
- Hardware: 1.0 (UK) / Firmware: 2.8.0<sup>\*</sup>
|
- Hardware: 1.0 (UK) / Firmware: 2.8.0<sup>\*</sup>
|
||||||
|
|
||||||
@ -208,10 +209,13 @@ All Tapo devices require authentication.<br>Hub-Connected Devices may work acros
|
|||||||
|
|
||||||
- **H100**
|
- **H100**
|
||||||
- Hardware: 1.0 (EU) / Firmware: 1.2.3
|
- Hardware: 1.0 (EU) / Firmware: 1.2.3
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.5.10
|
||||||
- Hardware: 1.0 (EU) / Firmware: 1.5.5
|
- Hardware: 1.0 (EU) / Firmware: 1.5.5
|
||||||
|
|
||||||
### Hub-Connected Devices
|
### Hub-Connected Devices
|
||||||
|
|
||||||
|
- **T110**
|
||||||
|
- Hardware: 1.0 (EU) / Firmware: 1.8.0
|
||||||
- **T300**
|
- **T300**
|
||||||
- Hardware: 1.0 (EU) / Firmware: 1.7.0
|
- Hardware: 1.0 (EU) / Firmware: 1.7.0
|
||||||
- **T310**
|
- **T310**
|
||||||
|
@ -19,7 +19,7 @@ app = typer.Typer()
|
|||||||
def create_fixtures(dev: IotDevice, outputdir: Path):
|
def create_fixtures(dev: IotDevice, outputdir: Path):
|
||||||
"""Iterate over supported modules and create version-specific fixture files."""
|
"""Iterate over supported modules and create version-specific fixture files."""
|
||||||
for name, module in dev.modules.items():
|
for name, module in dev.modules.items():
|
||||||
module_dir = outputdir / name
|
module_dir = outputdir / str(name)
|
||||||
if not module_dir.exists():
|
if not module_dir.exists():
|
||||||
module_dir.mkdir(exist_ok=True, parents=True)
|
module_dir.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
86
docs/source/contribute.md
Normal file
86
docs/source/contribute.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
You probably arrived to this page as you are interested in contributing to python-kasa in some form?
|
||||||
|
All types of contributions are very welcome, so thank you!
|
||||||
|
This page aims to help you to get started.
|
||||||
|
|
||||||
|
```{contents} Contents
|
||||||
|
:local:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting up the development environment
|
||||||
|
|
||||||
|
To get started, simply clone this repository and initialize the development environment.
|
||||||
|
We are using [poetry](https://python-poetry.org) for dependency management, so after cloning the repository simply execute
|
||||||
|
`poetry install` which will install all necessary packages and create a virtual environment for you.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/python-kasa/python-kasa.git
|
||||||
|
$ cd python-kasa
|
||||||
|
$ poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code-style checks
|
||||||
|
|
||||||
|
We use several tools to automatically check all contributions as part of our CI pipeline.
|
||||||
|
The simplest way to verify that everything is formatted properly
|
||||||
|
before creating a pull request, consider activating the pre-commit hooks by executing `pre-commit install`.
|
||||||
|
This will make sure that the checks are passing when you do a commit.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
You can also execute the pre-commit hooks on all files by executing `pre-commit run -a`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
You can run tests on the library by executing `pytest` in the source directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ poetry run pytest kasa
|
||||||
|
```
|
||||||
|
|
||||||
|
This will run the tests against the contributed example responses.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
You can also execute the tests against a real device using `pytest --ip <address>`.
|
||||||
|
Note that this will perform state changes on the device.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analyzing network captures
|
||||||
|
|
||||||
|
The simplest way to add support for a new device or to improve existing ones is to capture traffic between the mobile app and the device.
|
||||||
|
After capturing the traffic, you can either use the [softScheck's wireshark dissector](https://github.com/softScheck/tplink-smartplug#wireshark-dissector)
|
||||||
|
or the `parse_pcap.py` script contained inside the `devtools` directory.
|
||||||
|
Note, that this works currently only on kasa-branded devices which use port 9999 for communications.
|
||||||
|
|
||||||
|
## Contributing fixture files
|
||||||
|
|
||||||
|
One of the easiest ways to contribute is by creating a fixture file and uploading it for us.
|
||||||
|
These files will help us to improve the library and run tests against devices that we have no access to.
|
||||||
|
|
||||||
|
This library is tested against responses from real devices ("fixture files").
|
||||||
|
These files contain responses for selected, known device commands and are stored [in our test suite](https://github.com/python-kasa/python-kasa/tree/master/kasa/tests/fixtures).
|
||||||
|
|
||||||
|
You can generate these files by using the `dump_devinfo.py` script.
|
||||||
|
Note, that this script should be run inside the main source directory so that the generated files are stored in the correct directories.
|
||||||
|
The easiest way to do that is by doing:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/python-kasa/python-kasa.git
|
||||||
|
$ cd python-kasa
|
||||||
|
$ poetry install
|
||||||
|
$ poetry shell
|
||||||
|
$ python -m devtools.dump_devinfo --username <username> --password <password> --host 192.168.1.123
|
||||||
|
```
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
You can also execute the script against a network by using `--target`: `python -m devtools.dump_devinfo --target network 192.168.1.255`
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will run queries against the device, and prompt at the end if you want to save the results.
|
||||||
|
If you choose to do so, it will save the fixture files directly in their correct place to make it easy to create a pull request.
|
||||||
|
|
||||||
|
```{note}
|
||||||
|
When adding new fixture files, you should run `pre-commit run -a` to re-generate the list of supported devices.
|
||||||
|
You may need to adjust `device_fixtures.py` to add a new model into the correct device categories. Verify that test pass by executing `poetry run pytest kasa`.
|
||||||
|
```
|
@ -10,6 +10,7 @@
|
|||||||
discover
|
discover
|
||||||
smartdevice
|
smartdevice
|
||||||
design
|
design
|
||||||
|
contribute
|
||||||
smartbulb
|
smartbulb
|
||||||
smartplug
|
smartplug
|
||||||
smartdimmer
|
smartdimmer
|
||||||
|
@ -16,7 +16,6 @@ from importlib.metadata import version
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from kasa.bulb import Bulb
|
|
||||||
from kasa.credentials import Credentials
|
from kasa.credentials import Credentials
|
||||||
from kasa.device import Device
|
from kasa.device import Device
|
||||||
from kasa.device_type import DeviceType
|
from kasa.device_type import DeviceType
|
||||||
@ -36,13 +35,12 @@ from kasa.exceptions import (
|
|||||||
UnsupportedDeviceError,
|
UnsupportedDeviceError,
|
||||||
)
|
)
|
||||||
from kasa.feature import Feature
|
from kasa.feature import Feature
|
||||||
from kasa.firmware import Firmware, FirmwareUpdate
|
from kasa.interfaces.light import Light, LightPreset
|
||||||
from kasa.iot.iotbulb import BulbPreset, TurnOnBehavior, TurnOnBehaviors
|
|
||||||
from kasa.iotprotocol import (
|
from kasa.iotprotocol import (
|
||||||
IotProtocol,
|
IotProtocol,
|
||||||
_deprecated_TPLinkSmartHomeProtocol, # noqa: F401
|
_deprecated_TPLinkSmartHomeProtocol, # noqa: F401
|
||||||
)
|
)
|
||||||
from kasa.plug import Plug
|
from kasa.module import Module
|
||||||
from kasa.protocol import BaseProtocol
|
from kasa.protocol import BaseProtocol
|
||||||
from kasa.smartprotocol import SmartProtocol
|
from kasa.smartprotocol import SmartProtocol
|
||||||
|
|
||||||
@ -54,15 +52,16 @@ __all__ = [
|
|||||||
"BaseProtocol",
|
"BaseProtocol",
|
||||||
"IotProtocol",
|
"IotProtocol",
|
||||||
"SmartProtocol",
|
"SmartProtocol",
|
||||||
"BulbPreset",
|
"LightPreset",
|
||||||
"TurnOnBehaviors",
|
"TurnOnBehaviors",
|
||||||
"TurnOnBehavior",
|
"TurnOnBehavior",
|
||||||
"DeviceType",
|
"DeviceType",
|
||||||
"Feature",
|
"Feature",
|
||||||
"EmeterStatus",
|
"EmeterStatus",
|
||||||
"Device",
|
"Device",
|
||||||
"Bulb",
|
"Light",
|
||||||
"Plug",
|
"Plug",
|
||||||
|
"Module",
|
||||||
"KasaException",
|
"KasaException",
|
||||||
"AuthenticationError",
|
"AuthenticationError",
|
||||||
"DeviceError",
|
"DeviceError",
|
||||||
@ -73,8 +72,6 @@ __all__ = [
|
|||||||
"ConnectionType",
|
"ConnectionType",
|
||||||
"EncryptType",
|
"EncryptType",
|
||||||
"DeviceFamilyType",
|
"DeviceFamilyType",
|
||||||
"Firmware",
|
|
||||||
"FirmwareUpdate",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
from . import iot
|
from . import iot
|
||||||
@ -87,7 +84,7 @@ deprecated_smart_devices = {
|
|||||||
"SmartLightStrip": iot.IotLightStrip,
|
"SmartLightStrip": iot.IotLightStrip,
|
||||||
"SmartStrip": iot.IotStrip,
|
"SmartStrip": iot.IotStrip,
|
||||||
"SmartDimmer": iot.IotDimmer,
|
"SmartDimmer": iot.IotDimmer,
|
||||||
"SmartBulbPreset": BulbPreset,
|
"SmartBulbPreset": LightPreset,
|
||||||
}
|
}
|
||||||
deprecated_exceptions = {
|
deprecated_exceptions = {
|
||||||
"SmartDeviceException": KasaException,
|
"SmartDeviceException": KasaException,
|
||||||
@ -127,7 +124,7 @@ if TYPE_CHECKING:
|
|||||||
SmartLightStrip = iot.IotLightStrip
|
SmartLightStrip = iot.IotLightStrip
|
||||||
SmartStrip = iot.IotStrip
|
SmartStrip = iot.IotStrip
|
||||||
SmartDimmer = iot.IotDimmer
|
SmartDimmer = iot.IotDimmer
|
||||||
SmartBulbPreset = BulbPreset
|
SmartBulbPreset = LightPreset
|
||||||
|
|
||||||
SmartDeviceException = KasaException
|
SmartDeviceException = KasaException
|
||||||
UnsupportedDeviceException = UnsupportedDeviceError
|
UnsupportedDeviceException = UnsupportedDeviceError
|
||||||
|
82
kasa/cli.py
82
kasa/cli.py
@ -18,7 +18,6 @@ from pydantic.v1 import ValidationError
|
|||||||
|
|
||||||
from kasa import (
|
from kasa import (
|
||||||
AuthenticationError,
|
AuthenticationError,
|
||||||
Bulb,
|
|
||||||
ConnectionType,
|
ConnectionType,
|
||||||
Credentials,
|
Credentials,
|
||||||
Device,
|
Device,
|
||||||
@ -28,6 +27,7 @@ from kasa import (
|
|||||||
EncryptType,
|
EncryptType,
|
||||||
Feature,
|
Feature,
|
||||||
KasaException,
|
KasaException,
|
||||||
|
Module,
|
||||||
UnsupportedDeviceError,
|
UnsupportedDeviceError,
|
||||||
)
|
)
|
||||||
from kasa.discover import DiscoveryResult
|
from kasa.discover import DiscoveryResult
|
||||||
@ -859,18 +859,18 @@ async def usage(dev: Device, year, month, erase):
|
|||||||
@click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False)
|
@click.argument("brightness", type=click.IntRange(0, 100), default=None, required=False)
|
||||||
@click.option("--transition", type=int, required=False)
|
@click.option("--transition", type=int, required=False)
|
||||||
@pass_dev
|
@pass_dev
|
||||||
async def brightness(dev: Bulb, brightness: int, transition: int):
|
async def brightness(dev: Device, brightness: int, transition: int):
|
||||||
"""Get or set brightness."""
|
"""Get or set brightness."""
|
||||||
if not dev.is_dimmable:
|
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
|
||||||
echo("This device does not support brightness.")
|
echo("This device does not support brightness.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if brightness is None:
|
if brightness is None:
|
||||||
echo(f"Brightness: {dev.brightness}")
|
echo(f"Brightness: {light.brightness}")
|
||||||
return dev.brightness
|
return light.brightness
|
||||||
else:
|
else:
|
||||||
echo(f"Setting brightness to {brightness}")
|
echo(f"Setting brightness to {brightness}")
|
||||||
return await dev.set_brightness(brightness, transition=transition)
|
return await light.set_brightness(brightness, transition=transition)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@ -879,15 +879,15 @@ async def brightness(dev: Bulb, brightness: int, transition: int):
|
|||||||
)
|
)
|
||||||
@click.option("--transition", type=int, required=False)
|
@click.option("--transition", type=int, required=False)
|
||||||
@pass_dev
|
@pass_dev
|
||||||
async def temperature(dev: Bulb, temperature: int, transition: int):
|
async def temperature(dev: Device, temperature: int, transition: int):
|
||||||
"""Get or set color temperature."""
|
"""Get or set color temperature."""
|
||||||
if not dev.is_variable_color_temp:
|
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
|
||||||
echo("Device does not support color temperature")
|
echo("Device does not support color temperature")
|
||||||
return
|
return
|
||||||
|
|
||||||
if temperature is None:
|
if temperature is None:
|
||||||
echo(f"Color temperature: {dev.color_temp}")
|
echo(f"Color temperature: {light.color_temp}")
|
||||||
valid_temperature_range = dev.valid_temperature_range
|
valid_temperature_range = light.valid_temperature_range
|
||||||
if valid_temperature_range != (0, 0):
|
if valid_temperature_range != (0, 0):
|
||||||
echo("(min: {}, max: {})".format(*valid_temperature_range))
|
echo("(min: {}, max: {})".format(*valid_temperature_range))
|
||||||
else:
|
else:
|
||||||
@ -895,31 +895,34 @@ async def temperature(dev: Bulb, temperature: int, transition: int):
|
|||||||
"Temperature range unknown, please open a github issue"
|
"Temperature range unknown, please open a github issue"
|
||||||
f" or a pull request for model '{dev.model}'"
|
f" or a pull request for model '{dev.model}'"
|
||||||
)
|
)
|
||||||
return dev.valid_temperature_range
|
return light.valid_temperature_range
|
||||||
else:
|
else:
|
||||||
echo(f"Setting color temperature to {temperature}")
|
echo(f"Setting color temperature to {temperature}")
|
||||||
return await dev.set_color_temp(temperature, transition=transition)
|
return await light.set_color_temp(temperature, transition=transition)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument("effect", type=click.STRING, default=None, required=False)
|
@click.argument("effect", type=click.STRING, default=None, required=False)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@pass_dev
|
@pass_dev
|
||||||
async def effect(dev, ctx, effect):
|
async def effect(dev: Device, ctx, effect):
|
||||||
"""Set an effect."""
|
"""Set an effect."""
|
||||||
if not dev.has_effects:
|
if not (light_effect := dev.modules.get(Module.LightEffect)):
|
||||||
echo("Device does not support effects")
|
echo("Device does not support effects")
|
||||||
return
|
return
|
||||||
if effect is None:
|
if effect is None:
|
||||||
raise click.BadArgumentUsage(
|
raise click.BadArgumentUsage(
|
||||||
f"Setting an effect requires a named built-in effect: {dev.effect_list}",
|
"Setting an effect requires a named built-in effect: "
|
||||||
|
+ f"{light_effect.effect_list}",
|
||||||
ctx,
|
ctx,
|
||||||
)
|
)
|
||||||
if effect not in dev.effect_list:
|
if effect not in light_effect.effect_list:
|
||||||
raise click.BadArgumentUsage(f"Effect must be one of: {dev.effect_list}", ctx)
|
raise click.BadArgumentUsage(
|
||||||
|
f"Effect must be one of: {light_effect.effect_list}", ctx
|
||||||
|
)
|
||||||
|
|
||||||
echo(f"Setting Effect: {effect}")
|
echo(f"Setting Effect: {effect}")
|
||||||
return await dev.set_effect(effect)
|
return await light_effect.set_effect(effect)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@ -929,33 +932,36 @@ async def effect(dev, ctx, effect):
|
|||||||
@click.option("--transition", type=int, required=False)
|
@click.option("--transition", type=int, required=False)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@pass_dev
|
@pass_dev
|
||||||
async def hsv(dev, ctx, h, s, v, transition):
|
async def hsv(dev: Device, ctx, h, s, v, transition):
|
||||||
"""Get or set color in HSV."""
|
"""Get or set color in HSV."""
|
||||||
if not dev.is_color:
|
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
|
||||||
echo("Device does not support colors")
|
echo("Device does not support colors")
|
||||||
return
|
return
|
||||||
|
|
||||||
if h is None or s is None or v is None:
|
if h is None and s is None and v is None:
|
||||||
echo(f"Current HSV: {dev.hsv}")
|
echo(f"Current HSV: {light.hsv}")
|
||||||
return dev.hsv
|
return light.hsv
|
||||||
elif s is None or v is None:
|
elif s is None or v is None:
|
||||||
raise click.BadArgumentUsage("Setting a color requires 3 values.", ctx)
|
raise click.BadArgumentUsage("Setting a color requires 3 values.", ctx)
|
||||||
else:
|
else:
|
||||||
echo(f"Setting HSV: {h} {s} {v}")
|
echo(f"Setting HSV: {h} {s} {v}")
|
||||||
return await dev.set_hsv(h, s, v, transition=transition)
|
return await light.set_hsv(h, s, v, transition=transition)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument("state", type=bool, required=False)
|
@click.argument("state", type=bool, required=False)
|
||||||
@pass_dev
|
@pass_dev
|
||||||
async def led(dev, state):
|
async def led(dev: Device, state):
|
||||||
"""Get or set (Plug's) led state."""
|
"""Get or set (Plug's) led state."""
|
||||||
|
if not (led := dev.modules.get(Module.Led)):
|
||||||
|
echo("Device does not support led.")
|
||||||
|
return
|
||||||
if state is not None:
|
if state is not None:
|
||||||
echo(f"Turning led to {state}")
|
echo(f"Turning led to {state}")
|
||||||
return await dev.set_led(state)
|
return await led.set_led(state)
|
||||||
else:
|
else:
|
||||||
echo(f"LED state: {dev.led}")
|
echo(f"LED state: {led.led}")
|
||||||
return dev.led
|
return led.led
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@ -975,8 +981,8 @@ async def time(dev):
|
|||||||
async def on(dev: Device, index: int, name: str, transition: int):
|
async def on(dev: Device, index: int, name: str, transition: int):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
if index is not None or name is not None:
|
if index is not None or name is not None:
|
||||||
if not dev.is_strip:
|
if not dev.children:
|
||||||
echo("Index and name are only for power strips!")
|
echo("Index and name are only for devices with children.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if index is not None:
|
if index is not None:
|
||||||
@ -996,8 +1002,8 @@ async def on(dev: Device, index: int, name: str, transition: int):
|
|||||||
async def off(dev: Device, index: int, name: str, transition: int):
|
async def off(dev: Device, index: int, name: str, transition: int):
|
||||||
"""Turn the device off."""
|
"""Turn the device off."""
|
||||||
if index is not None or name is not None:
|
if index is not None or name is not None:
|
||||||
if not dev.is_strip:
|
if not dev.children:
|
||||||
echo("Index and name are only for power strips!")
|
echo("Index and name are only for devices with children.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if index is not None:
|
if index is not None:
|
||||||
@ -1017,8 +1023,8 @@ async def off(dev: Device, index: int, name: str, transition: int):
|
|||||||
async def toggle(dev: Device, index: int, name: str, transition: int):
|
async def toggle(dev: Device, index: int, name: str, transition: int):
|
||||||
"""Toggle the device on/off."""
|
"""Toggle the device on/off."""
|
||||||
if index is not None or name is not None:
|
if index is not None or name is not None:
|
||||||
if not dev.is_strip:
|
if not dev.children:
|
||||||
echo("Index and name are only for power strips!")
|
echo("Index and name are only for devices with children.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if index is not None:
|
if index is not None:
|
||||||
@ -1266,7 +1272,11 @@ async def firmware(ctx: click.Context, dev: Device):
|
|||||||
@click.pass_context
|
@click.pass_context
|
||||||
async def firmware_info(ctx: click.Context, dev: Device):
|
async def firmware_info(ctx: click.Context, dev: Device):
|
||||||
"""Return firmware information."""
|
"""Return firmware information."""
|
||||||
res = await dev.firmware.check_for_updates()
|
if not (firmware := dev.modules.get(Module.Firmware)):
|
||||||
|
echo("This device does not support firmware info.")
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await firmware.check_for_updates()
|
||||||
if res.update_available:
|
if res.update_available:
|
||||||
echo("[green bold]Update available![/green bold]")
|
echo("[green bold]Update available![/green bold]")
|
||||||
echo(f"Current firmware: {res.current_version}")
|
echo(f"Current firmware: {res.current_version}")
|
||||||
@ -1291,7 +1301,7 @@ async def firmware_update(ctx: click.Context, dev: Device):
|
|||||||
echo(f"Progress: {x}")
|
echo(f"Progress: {x}")
|
||||||
|
|
||||||
echo("Going to update %s", dev)
|
echo("Going to update %s", dev)
|
||||||
await dev.firmware.update_firmware(progress_cb=progress) # type: ignore
|
await dev.modules.get[Module.Firmware].update_firmware(progress_cb=progress) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
133
kasa/device.py
133
kasa/device.py
@ -6,7 +6,8 @@ import logging
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Mapping, Sequence, overload
|
from typing import TYPE_CHECKING, Any, Mapping, Sequence
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from .credentials import Credentials
|
from .credentials import Credentials
|
||||||
from .device_type import DeviceType
|
from .device_type import DeviceType
|
||||||
@ -14,12 +15,14 @@ from .deviceconfig import DeviceConfig
|
|||||||
from .emeterstatus import EmeterStatus
|
from .emeterstatus import EmeterStatus
|
||||||
from .exceptions import KasaException
|
from .exceptions import KasaException
|
||||||
from .feature import Feature
|
from .feature import Feature
|
||||||
from .firmware import Firmware
|
|
||||||
from .iotprotocol import IotProtocol
|
from .iotprotocol import IotProtocol
|
||||||
from .module import Module, ModuleT
|
from .module import Module
|
||||||
from .protocol import BaseProtocol
|
from .protocol import BaseProtocol
|
||||||
from .xortransport import XorTransport
|
from .xortransport import XorTransport
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .modulemapping import ModuleMapping
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WifiNetwork:
|
class WifiNetwork:
|
||||||
@ -114,21 +117,9 @@ class Device(ABC):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def modules(self) -> Mapping[str, Module]:
|
def modules(self) -> ModuleMapping[Module]:
|
||||||
"""Return the device modules."""
|
"""Return the device modules."""
|
||||||
|
|
||||||
@overload
|
|
||||||
@abstractmethod
|
|
||||||
def get_module(self, module_type: type[ModuleT]) -> ModuleT | None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
@abstractmethod
|
|
||||||
def get_module(self, module_type: str) -> Module | None: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_module(self, module_type: type[ModuleT] | str) -> ModuleT | Module | None:
|
|
||||||
"""Return the module from the device modules or None if not present."""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -218,61 +209,6 @@ class Device(ABC):
|
|||||||
def sys_info(self) -> dict[str, Any]:
|
def sys_info(self) -> dict[str, Any]:
|
||||||
"""Returns the device info."""
|
"""Returns the device info."""
|
||||||
|
|
||||||
@property
|
|
||||||
def is_bulb(self) -> bool:
|
|
||||||
"""Return True if the device is a bulb."""
|
|
||||||
return self.device_type == DeviceType.Bulb
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_light_strip(self) -> bool:
|
|
||||||
"""Return True if the device is a led strip."""
|
|
||||||
return self.device_type == DeviceType.LightStrip
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_plug(self) -> bool:
|
|
||||||
"""Return True if the device is a 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
|
|
||||||
def is_strip(self) -> bool:
|
|
||||||
"""Return True if the device is a strip."""
|
|
||||||
return self.device_type == DeviceType.Strip
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_strip_socket(self) -> bool:
|
|
||||||
"""Return True if the device is a strip socket."""
|
|
||||||
return self.device_type == DeviceType.StripSocket
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dimmer(self) -> bool:
|
|
||||||
"""Return True if the device is a dimmer."""
|
|
||||||
return self.device_type == DeviceType.Dimmer
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dimmable(self) -> bool:
|
|
||||||
"""Return True if the device is dimmable."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_fan(self) -> bool:
|
|
||||||
"""Return True if the device is a fan."""
|
|
||||||
return self.device_type == DeviceType.Fan
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_variable_color_temp(self) -> bool:
|
|
||||||
"""Return True if the device supports color temperature."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_color(self) -> bool:
|
|
||||||
"""Return True if the device supports color changes."""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_plug_by_name(self, name: str) -> Device:
|
def get_plug_by_name(self, name: str) -> Device:
|
||||||
"""Return child device for the given name."""
|
"""Return child device for the given name."""
|
||||||
for p in self.children:
|
for p in self.children:
|
||||||
@ -289,11 +225,6 @@ class Device(ABC):
|
|||||||
)
|
)
|
||||||
return self.children[index]
|
return self.children[index]
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def firmware(self) -> Firmware:
|
|
||||||
"""Return firmware."""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def time(self) -> datetime:
|
def time(self) -> datetime:
|
||||||
@ -398,3 +329,53 @@ class Device(ABC):
|
|||||||
if self._last_update is None:
|
if self._last_update is None:
|
||||||
return f"<{self.device_type} at {self.host} - update() needed>"
|
return f"<{self.device_type} at {self.host} - update() needed>"
|
||||||
return f"<{self.device_type} at {self.host} - {self.alias} ({self.model})>"
|
return f"<{self.device_type} at {self.host} - {self.alias} ({self.model})>"
|
||||||
|
|
||||||
|
_deprecated_attributes = {
|
||||||
|
# is_type
|
||||||
|
"is_bulb": (Module.Light, lambda self: self.device_type == DeviceType.Bulb),
|
||||||
|
"is_dimmer": (
|
||||||
|
Module.Light,
|
||||||
|
lambda self: self.device_type == DeviceType.Dimmer,
|
||||||
|
),
|
||||||
|
"is_light_strip": (
|
||||||
|
Module.LightEffect,
|
||||||
|
lambda self: self.device_type == DeviceType.LightStrip,
|
||||||
|
),
|
||||||
|
"is_plug": (Module.Led, lambda self: self.device_type == DeviceType.Plug),
|
||||||
|
"is_wallswitch": (
|
||||||
|
Module.Led,
|
||||||
|
lambda self: self.device_type == DeviceType.WallSwitch,
|
||||||
|
),
|
||||||
|
"is_strip": (None, lambda self: self.device_type == DeviceType.Strip),
|
||||||
|
"is_strip_socket": (
|
||||||
|
None,
|
||||||
|
lambda self: self.device_type == DeviceType.StripSocket,
|
||||||
|
), # TODO
|
||||||
|
# is_light_function
|
||||||
|
"is_color": (
|
||||||
|
Module.Light,
|
||||||
|
lambda self: Module.Light in self.modules
|
||||||
|
and self.modules[Module.Light].is_color,
|
||||||
|
),
|
||||||
|
"is_dimmable": (
|
||||||
|
Module.Light,
|
||||||
|
lambda self: Module.Light in self.modules
|
||||||
|
and self.modules[Module.Light].is_dimmable,
|
||||||
|
),
|
||||||
|
"is_variable_color_temp": (
|
||||||
|
Module.Light,
|
||||||
|
lambda self: Module.Light in self.modules
|
||||||
|
and self.modules[Module.Light].is_variable_color_temp,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __getattr__(self, name) -> bool:
|
||||||
|
if name in self._deprecated_attributes:
|
||||||
|
module = self._deprecated_attributes[name][0]
|
||||||
|
func = self._deprecated_attributes[name][1]
|
||||||
|
msg = f"{name} is deprecated"
|
||||||
|
if module:
|
||||||
|
msg += f", use: {module} in device.modules instead"
|
||||||
|
warn(msg, DeprecationWarning, stacklevel=1)
|
||||||
|
return func(self)
|
||||||
|
raise AttributeError(f"Device has no attribute {name!r}")
|
||||||
|
16
kasa/interfaces/__init__.py
Normal file
16
kasa/interfaces/__init__.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""Package for interfaces."""
|
||||||
|
|
||||||
|
from .fan import Fan
|
||||||
|
from .firmware import Firmware
|
||||||
|
from .led import Led
|
||||||
|
from .light import Light, LightPreset
|
||||||
|
from .lighteffect import LightEffect
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Fan",
|
||||||
|
"Firmware",
|
||||||
|
"Led",
|
||||||
|
"Light",
|
||||||
|
"LightEffect",
|
||||||
|
"LightPreset",
|
||||||
|
]
|
@ -4,10 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
from .device import Device
|
from ..module import Module
|
||||||
|
|
||||||
|
|
||||||
class Fan(Device, ABC):
|
class Fan(Module, ABC):
|
||||||
"""Interface for a Fan."""
|
"""Interface for a Fan."""
|
||||||
|
|
||||||
@property
|
@property
|
@ -5,13 +5,25 @@ from __future__ import annotations
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Any, Awaitable, Callable
|
from typing import Callable, Coroutine
|
||||||
|
|
||||||
|
from ..module import Module
|
||||||
|
|
||||||
UpdateResult = bool
|
UpdateResult = bool
|
||||||
|
|
||||||
|
|
||||||
|
class FirmwareDownloadState(ABC):
|
||||||
|
"""Download state."""
|
||||||
|
|
||||||
|
status: int
|
||||||
|
progress: int
|
||||||
|
reboot_time: int
|
||||||
|
upgrade_time: int
|
||||||
|
auto_upgrade: bool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FirmwareUpdate:
|
class FirmwareUpdateInfo:
|
||||||
"""Update info status object."""
|
"""Update info status object."""
|
||||||
|
|
||||||
update_available: bool | None = None
|
update_available: bool | None = None
|
||||||
@ -21,21 +33,19 @@ class FirmwareUpdate:
|
|||||||
release_notes: str | None = None
|
release_notes: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class Firmware(ABC):
|
class Firmware(Module, ABC):
|
||||||
"""Interface to access firmware information and perform updates."""
|
"""Interface to access firmware information and perform updates."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_firmware(
|
async def update_firmware(
|
||||||
self, *, progress_cb: Callable[[Any, Any], Awaitable]
|
self, *, progress_cb: Callable[[FirmwareDownloadState], Coroutine] | None = None
|
||||||
) -> UpdateResult:
|
) -> UpdateResult:
|
||||||
"""Perform firmware update.
|
"""Perform firmware update.
|
||||||
|
|
||||||
This "blocks" until the update process has finished.
|
This "blocks" until the update process has finished.
|
||||||
You can set *progress_cb* to get progress updates.
|
You can set *progress_cb* to get progress updates.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def check_for_updates(self) -> FirmwareUpdate:
|
async def check_for_updates(self) -> FirmwareUpdateInfo:
|
||||||
"""Return information about available updates."""
|
"""Return firmware update information."""
|
||||||
raise NotImplementedError
|
|
38
kasa/interfaces/led.py
Normal file
38
kasa/interfaces/led.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""Module for base light effect module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from ..feature import Feature
|
||||||
|
from ..module import Module
|
||||||
|
|
||||||
|
|
||||||
|
class Led(Module, ABC):
|
||||||
|
"""Base interface to represent a LED module."""
|
||||||
|
|
||||||
|
def _initialize_features(self):
|
||||||
|
"""Initialize features."""
|
||||||
|
device = self._device
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device=device,
|
||||||
|
container=self,
|
||||||
|
name="LED",
|
||||||
|
id="led",
|
||||||
|
icon="mdi:led",
|
||||||
|
attribute_getter="led",
|
||||||
|
attribute_setter="set_led",
|
||||||
|
type=Feature.Type.Switch,
|
||||||
|
category=Feature.Category.Config,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def led(self) -> bool:
|
||||||
|
"""Return current led status."""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def set_led(self, enable: bool) -> None:
|
||||||
|
"""Set led."""
|
@ -7,7 +7,7 @@ from typing import NamedTuple, Optional
|
|||||||
|
|
||||||
from pydantic.v1 import BaseModel
|
from pydantic.v1 import BaseModel
|
||||||
|
|
||||||
from .device import Device
|
from ..module import Module
|
||||||
|
|
||||||
|
|
||||||
class ColorTempRange(NamedTuple):
|
class ColorTempRange(NamedTuple):
|
||||||
@ -25,8 +25,8 @@ class HSV(NamedTuple):
|
|||||||
value: int
|
value: int
|
||||||
|
|
||||||
|
|
||||||
class BulbPreset(BaseModel):
|
class LightPreset(BaseModel):
|
||||||
"""Bulb configuration preset."""
|
"""Light configuration preset."""
|
||||||
|
|
||||||
index: int
|
index: int
|
||||||
brightness: int
|
brightness: int
|
||||||
@ -42,23 +42,19 @@ class BulbPreset(BaseModel):
|
|||||||
mode: Optional[int] # noqa: UP007
|
mode: Optional[int] # noqa: UP007
|
||||||
|
|
||||||
|
|
||||||
class Bulb(Device, ABC):
|
class Light(Module, ABC):
|
||||||
"""Base class for TP-Link Bulb."""
|
"""Base class for TP-Link Light."""
|
||||||
|
|
||||||
def _raise_for_invalid_brightness(self, value):
|
@property
|
||||||
if not isinstance(value, int) or not (0 <= value <= 100):
|
@abstractmethod
|
||||||
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
|
def is_dimmable(self) -> bool:
|
||||||
|
"""Whether the light supports brightness changes."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_color(self) -> bool:
|
def is_color(self) -> bool:
|
||||||
"""Whether the bulb supports color changes."""
|
"""Whether the bulb supports color changes."""
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def is_dimmable(self) -> bool:
|
|
||||||
"""Whether the bulb supports brightness changes."""
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_variable_color_temp(self) -> bool:
|
def is_variable_color_temp(self) -> bool:
|
||||||
@ -137,8 +133,3 @@ class Bulb(Device, ABC):
|
|||||||
:param int brightness: brightness in percent
|
:param int brightness: brightness in percent
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
|
||||||
@abstractmethod
|
|
||||||
def presets(self) -> list[BulbPreset]:
|
|
||||||
"""Return a list of available bulb setting presets."""
|
|
80
kasa/interfaces/lighteffect.py
Normal file
80
kasa/interfaces/lighteffect.py
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"""Module for base light effect module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from ..feature import Feature
|
||||||
|
from ..module import Module
|
||||||
|
|
||||||
|
|
||||||
|
class LightEffect(Module, ABC):
|
||||||
|
"""Interface to represent a light effect module."""
|
||||||
|
|
||||||
|
LIGHT_EFFECTS_OFF = "Off"
|
||||||
|
|
||||||
|
def _initialize_features(self):
|
||||||
|
"""Initialize features."""
|
||||||
|
device = self._device
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device,
|
||||||
|
id="light_effect",
|
||||||
|
name="Light effect",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="effect",
|
||||||
|
attribute_setter="set_effect",
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Type.Choice,
|
||||||
|
choices_getter="effect_list",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def has_custom_effects(self) -> bool:
|
||||||
|
"""Return True if the device supports setting custom effects."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def effect(self) -> str:
|
||||||
|
"""Return effect state or name."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def effect_list(self) -> list[str]:
|
||||||
|
"""Return built-in effects list.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
['Aurora', 'Bubbling Cauldron', ...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def set_effect(
|
||||||
|
self,
|
||||||
|
effect: str,
|
||||||
|
*,
|
||||||
|
brightness: int | None = None,
|
||||||
|
transition: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set an effect on the device.
|
||||||
|
|
||||||
|
If brightness or transition is defined,
|
||||||
|
its value will be used instead of the effect-specific default.
|
||||||
|
|
||||||
|
See :meth:`effect_list` for available effects,
|
||||||
|
or use :meth:`set_custom_effect` for custom effects.
|
||||||
|
|
||||||
|
:param str effect: The effect to set
|
||||||
|
:param int brightness: The wanted brightness
|
||||||
|
:param int transition: The wanted transition time
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def set_custom_effect(
|
||||||
|
self,
|
||||||
|
effect_dict: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Set a custom effect on the device.
|
||||||
|
|
||||||
|
:param str effect_dict: The custom effect dict to set
|
||||||
|
"""
|
@ -9,13 +9,22 @@ from typing import Optional, cast
|
|||||||
|
|
||||||
from pydantic.v1 import BaseModel, Field, root_validator
|
from pydantic.v1 import BaseModel, Field, root_validator
|
||||||
|
|
||||||
from ..bulb import HSV, Bulb, BulbPreset, ColorTempRange
|
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..feature import Feature
|
from ..interfaces.light import HSV, ColorTempRange, LightPreset
|
||||||
|
from ..module import Module
|
||||||
from ..protocol import BaseProtocol
|
from ..protocol import BaseProtocol
|
||||||
from .iotdevice import IotDevice, KasaException, requires_update
|
from .iotdevice import IotDevice, KasaException, requires_update
|
||||||
from .modules import Antitheft, Cloud, Countdown, Emeter, Schedule, Time, Usage
|
from .modules import (
|
||||||
|
Antitheft,
|
||||||
|
Cloud,
|
||||||
|
Countdown,
|
||||||
|
Emeter,
|
||||||
|
Light,
|
||||||
|
Schedule,
|
||||||
|
Time,
|
||||||
|
Usage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BehaviorMode(str, Enum):
|
class BehaviorMode(str, Enum):
|
||||||
@ -87,7 +96,7 @@ NON_COLOR_MODE_FLAGS = {"transition_period", "on_off"}
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class IotBulb(IotDevice, Bulb):
|
class IotBulb(IotDevice):
|
||||||
r"""Representation of a TP-Link Smart Bulb.
|
r"""Representation of a TP-Link Smart Bulb.
|
||||||
|
|
||||||
To initialize, you have to await :func:`update()` at least once.
|
To initialize, you have to await :func:`update()` at least once.
|
||||||
@ -169,9 +178,9 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
Bulb configuration presets can be accessed using the :func:`presets` property:
|
Bulb configuration presets can be accessed using the :func:`presets` property:
|
||||||
|
|
||||||
>>> bulb.presets
|
>>> bulb.presets
|
||||||
[BulbPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), BulbPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), BulbPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), BulbPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
|
[LightPreset(index=0, brightness=50, hue=0, saturation=0, color_temp=2700, custom=None, id=None, mode=None), LightPreset(index=1, brightness=100, hue=0, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=2, brightness=100, hue=120, saturation=75, color_temp=0, custom=None, id=None, mode=None), LightPreset(index=3, brightness=100, hue=240, saturation=75, color_temp=0, custom=None, id=None, mode=None)]
|
||||||
|
|
||||||
To modify an existing preset, pass :class:`~kasa.smartbulb.SmartBulbPreset`
|
To modify an existing preset, pass :class:`~kasa.smartbulb.LightPreset`
|
||||||
instance to :func:`save_preset` method:
|
instance to :func:`save_preset` method:
|
||||||
|
|
||||||
>>> preset = bulb.presets[0]
|
>>> preset = bulb.presets[0]
|
||||||
@ -198,64 +207,40 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(host=host, config=config, protocol=protocol)
|
super().__init__(host=host, config=config, protocol=protocol)
|
||||||
self._device_type = DeviceType.Bulb
|
self._device_type = DeviceType.Bulb
|
||||||
self.add_module("schedule", Schedule(self, "smartlife.iot.common.schedule"))
|
|
||||||
self.add_module("usage", Usage(self, "smartlife.iot.common.schedule"))
|
|
||||||
self.add_module("antitheft", Antitheft(self, "smartlife.iot.common.anti_theft"))
|
|
||||||
self.add_module("time", Time(self, "smartlife.iot.common.timesetting"))
|
|
||||||
self.add_module("emeter", Emeter(self, self.emeter_type))
|
|
||||||
self.add_module("countdown", Countdown(self, "countdown"))
|
|
||||||
self.add_module("cloud", Cloud(self, "smartlife.iot.common.cloud"))
|
|
||||||
|
|
||||||
async def _initialize_features(self):
|
async def _initialize_modules(self):
|
||||||
await super()._initialize_features()
|
"""Initialize modules not added in init."""
|
||||||
|
await super()._initialize_modules()
|
||||||
if bool(self.sys_info["is_dimmable"]): # pragma: no branch
|
self.add_module(
|
||||||
self._add_feature(
|
Module.IotSchedule, Schedule(self, "smartlife.iot.common.schedule")
|
||||||
Feature(
|
|
||||||
device=self,
|
|
||||||
id="brightness",
|
|
||||||
name="Brightness",
|
|
||||||
attribute_getter="brightness",
|
|
||||||
attribute_setter="set_brightness",
|
|
||||||
minimum_value=1,
|
|
||||||
maximum_value=100,
|
|
||||||
type=Feature.Type.Number,
|
|
||||||
category=Feature.Category.Primary,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.is_variable_color_temp:
|
|
||||||
self._add_feature(
|
|
||||||
Feature(
|
|
||||||
device=self,
|
|
||||||
id="color_temperature",
|
|
||||||
name="Color temperature",
|
|
||||||
container=self,
|
|
||||||
attribute_getter="color_temp",
|
|
||||||
attribute_setter="set_color_temp",
|
|
||||||
range_getter="valid_temperature_range",
|
|
||||||
category=Feature.Category.Primary,
|
|
||||||
type=Feature.Type.Number,
|
|
||||||
)
|
)
|
||||||
|
self.add_module(Module.IotUsage, Usage(self, "smartlife.iot.common.schedule"))
|
||||||
|
self.add_module(
|
||||||
|
Module.IotAntitheft, Antitheft(self, "smartlife.iot.common.anti_theft")
|
||||||
)
|
)
|
||||||
|
self.add_module(Module.IotTime, Time(self, "smartlife.iot.common.timesetting"))
|
||||||
|
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
|
||||||
|
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
|
||||||
|
self.add_module(Module.IotCloud, Cloud(self, "smartlife.iot.common.cloud"))
|
||||||
|
self.add_module(Module.Light, Light(self, "light"))
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def is_color(self) -> bool:
|
def _is_color(self) -> bool:
|
||||||
"""Whether the bulb supports color changes."""
|
"""Whether the bulb supports color changes."""
|
||||||
sys_info = self.sys_info
|
sys_info = self.sys_info
|
||||||
return bool(sys_info["is_color"])
|
return bool(sys_info["is_color"])
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def is_dimmable(self) -> bool:
|
def _is_dimmable(self) -> bool:
|
||||||
"""Whether the bulb supports brightness changes."""
|
"""Whether the bulb supports brightness changes."""
|
||||||
sys_info = self.sys_info
|
sys_info = self.sys_info
|
||||||
return bool(sys_info["is_dimmable"])
|
return bool(sys_info["is_dimmable"])
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def is_variable_color_temp(self) -> bool:
|
def _is_variable_color_temp(self) -> bool:
|
||||||
"""Whether the bulb supports color temperature changes."""
|
"""Whether the bulb supports color temperature changes."""
|
||||||
sys_info = self.sys_info
|
sys_info = self.sys_info
|
||||||
return bool(sys_info["is_variable_color_temp"])
|
return bool(sys_info["is_variable_color_temp"])
|
||||||
@ -267,7 +252,7 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
|
|
||||||
:return: White temperature range in Kelvin (minimum, maximum)
|
:return: White temperature range in Kelvin (minimum, maximum)
|
||||||
"""
|
"""
|
||||||
if not self.is_variable_color_temp:
|
if not self._is_variable_color_temp:
|
||||||
raise KasaException("Color temperature not supported")
|
raise KasaException("Color temperature not supported")
|
||||||
|
|
||||||
for model, temp_range in TPLINK_KELVIN.items():
|
for model, temp_range in TPLINK_KELVIN.items():
|
||||||
@ -367,7 +352,7 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
|
|
||||||
:return: hue, saturation and value (degrees, %, %)
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
"""
|
"""
|
||||||
if not self.is_color:
|
if not self._is_color:
|
||||||
raise KasaException("Bulb does not support color.")
|
raise KasaException("Bulb does not support color.")
|
||||||
|
|
||||||
light_state = cast(dict, self.light_state)
|
light_state = cast(dict, self.light_state)
|
||||||
@ -394,7 +379,7 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
:param int value: value in percentage [0, 100]
|
:param int value: value in percentage [0, 100]
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
if not self.is_color:
|
if not self._is_color:
|
||||||
raise KasaException("Bulb does not support color.")
|
raise KasaException("Bulb does not support color.")
|
||||||
|
|
||||||
if not isinstance(hue, int) or not (0 <= hue <= 360):
|
if not isinstance(hue, int) or not (0 <= hue <= 360):
|
||||||
@ -421,7 +406,7 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
@requires_update
|
@requires_update
|
||||||
def color_temp(self) -> int:
|
def color_temp(self) -> int:
|
||||||
"""Return color temperature of the device in kelvin."""
|
"""Return color temperature of the device in kelvin."""
|
||||||
if not self.is_variable_color_temp:
|
if not self._is_variable_color_temp:
|
||||||
raise KasaException("Bulb does not support colortemp.")
|
raise KasaException("Bulb does not support colortemp.")
|
||||||
|
|
||||||
light_state = self.light_state
|
light_state = self.light_state
|
||||||
@ -436,7 +421,7 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
:param int temp: The new color temperature, in Kelvin
|
:param int temp: The new color temperature, in Kelvin
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
if not self.is_variable_color_temp:
|
if not self._is_variable_color_temp:
|
||||||
raise KasaException("Bulb does not support colortemp.")
|
raise KasaException("Bulb does not support colortemp.")
|
||||||
|
|
||||||
valid_temperature_range = self.valid_temperature_range
|
valid_temperature_range = self.valid_temperature_range
|
||||||
@ -453,11 +438,15 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
|
|
||||||
return await self.set_light_state(light_state, transition=transition)
|
return await self.set_light_state(light_state, transition=transition)
|
||||||
|
|
||||||
|
def _raise_for_invalid_brightness(self, value):
|
||||||
|
if not isinstance(value, int) or not (0 <= value <= 100):
|
||||||
|
raise ValueError(f"Invalid brightness value: {value} (valid range: 0-100%)")
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def brightness(self) -> int:
|
def brightness(self) -> int:
|
||||||
"""Return the current brightness in percentage."""
|
"""Return the current brightness in percentage."""
|
||||||
if not self.is_dimmable: # pragma: no cover
|
if not self._is_dimmable: # pragma: no cover
|
||||||
raise KasaException("Bulb is not dimmable.")
|
raise KasaException("Bulb is not dimmable.")
|
||||||
|
|
||||||
light_state = self.light_state
|
light_state = self.light_state
|
||||||
@ -472,7 +461,7 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
:param int brightness: brightness in percent
|
:param int brightness: brightness in percent
|
||||||
:param int transition: transition in milliseconds.
|
:param int transition: transition in milliseconds.
|
||||||
"""
|
"""
|
||||||
if not self.is_dimmable: # pragma: no cover
|
if not self._is_dimmable: # pragma: no cover
|
||||||
raise KasaException("Bulb is not dimmable.")
|
raise KasaException("Bulb is not dimmable.")
|
||||||
|
|
||||||
self._raise_for_invalid_brightness(brightness)
|
self._raise_for_invalid_brightness(brightness)
|
||||||
@ -518,11 +507,11 @@ class IotBulb(IotDevice, Bulb):
|
|||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def presets(self) -> list[BulbPreset]:
|
def presets(self) -> list[LightPreset]:
|
||||||
"""Return a list of available bulb setting presets."""
|
"""Return a list of available bulb setting presets."""
|
||||||
return [BulbPreset(**vals) for vals in self.sys_info["preferred_state"]]
|
return [LightPreset(**vals) for vals in self.sys_info["preferred_state"]]
|
||||||
|
|
||||||
async def save_preset(self, preset: BulbPreset):
|
async def save_preset(self, preset: LightPreset):
|
||||||
"""Save a setting preset.
|
"""Save a setting preset.
|
||||||
|
|
||||||
You can either construct a preset object manually, or pass an existing one
|
You can either construct a preset object manually, or pass an existing one
|
||||||
|
@ -19,18 +19,18 @@ import functools
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Mapping, Sequence, cast, overload
|
from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast
|
||||||
|
|
||||||
from ..device import Device, WifiNetwork
|
from ..device import Device, WifiNetwork
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..emeterstatus import EmeterStatus
|
from ..emeterstatus import EmeterStatus
|
||||||
from ..exceptions import KasaException
|
from ..exceptions import KasaException
|
||||||
from ..feature import Feature
|
from ..feature import Feature
|
||||||
from ..firmware import Firmware
|
from ..module import Module
|
||||||
from ..module import ModuleT
|
from ..modulemapping import ModuleMapping, ModuleName
|
||||||
from ..protocol import BaseProtocol
|
from ..protocol import BaseProtocol
|
||||||
from .iotmodule import IotModule
|
from .iotmodule import IotModule
|
||||||
from .modules import Emeter, Time
|
from .modules import Emeter
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -191,7 +191,7 @@ class IotDevice(Device):
|
|||||||
self._supported_modules: dict[str, IotModule] | None = None
|
self._supported_modules: dict[str, IotModule] | None = None
|
||||||
self._legacy_features: set[str] = set()
|
self._legacy_features: set[str] = set()
|
||||||
self._children: Mapping[str, IotDevice] = {}
|
self._children: Mapping[str, IotDevice] = {}
|
||||||
self._modules: dict[str, IotModule] = {}
|
self._modules: dict[str | ModuleName[Module], IotModule] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def children(self) -> Sequence[IotDevice]:
|
def children(self) -> Sequence[IotDevice]:
|
||||||
@ -199,38 +199,20 @@ class IotDevice(Device):
|
|||||||
return list(self._children.values())
|
return list(self._children.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def modules(self) -> dict[str, IotModule]:
|
def modules(self) -> ModuleMapping[IotModule]:
|
||||||
"""Return the device modules."""
|
"""Return the device modules."""
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
return cast(ModuleMapping[IotModule], self._modules)
|
||||||
return self._modules
|
return self._modules
|
||||||
|
|
||||||
@overload
|
def add_module(self, name: str | ModuleName[Module], module: IotModule):
|
||||||
def get_module(self, module_type: type[ModuleT]) -> ModuleT | None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def get_module(self, module_type: str) -> IotModule | None: ...
|
|
||||||
|
|
||||||
def get_module(
|
|
||||||
self, module_type: type[ModuleT] | str
|
|
||||||
) -> ModuleT | IotModule | None:
|
|
||||||
"""Return the module from the device modules or None if not present."""
|
|
||||||
if isinstance(module_type, str):
|
|
||||||
module_name = module_type.lower()
|
|
||||||
elif issubclass(module_type, IotModule):
|
|
||||||
module_name = module_type.__name__.lower()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
if module_name in self.modules:
|
|
||||||
return self.modules[module_name]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def add_module(self, name: str, module: IotModule):
|
|
||||||
"""Register a module."""
|
"""Register a module."""
|
||||||
if name in self.modules:
|
if name in self.modules:
|
||||||
_LOGGER.debug("Module %s already registered, ignoring..." % name)
|
_LOGGER.debug("Module %s already registered, ignoring..." % name)
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Adding module %s", module)
|
_LOGGER.debug("Adding module %s", module)
|
||||||
self.modules[name] = module
|
self._modules[name] = module
|
||||||
|
|
||||||
def _create_request(
|
def _create_request(
|
||||||
self, target: str, cmd: str, arg: dict | None = None, child_ids=None
|
self, target: str, cmd: str, arg: dict | None = None, child_ids=None
|
||||||
@ -292,11 +274,11 @@ class IotDevice(Device):
|
|||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def supported_modules(self) -> list[str]:
|
def supported_modules(self) -> list[str | ModuleName[Module]]:
|
||||||
"""Return a set of modules supported by the device."""
|
"""Return a set of modules supported by the device."""
|
||||||
# TODO: this should rather be called `features`, but we don't want to break
|
# TODO: this should rather be called `features`, but we don't want to break
|
||||||
# the API now. Maybe just deprecate it and point the users to use this?
|
# the API now. Maybe just deprecate it and point the users to use this?
|
||||||
return list(self.modules.keys())
|
return list(self._modules.keys())
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -325,12 +307,19 @@ class IotDevice(Device):
|
|||||||
self._last_update = response
|
self._last_update = response
|
||||||
self._set_sys_info(response["system"]["get_sysinfo"])
|
self._set_sys_info(response["system"]["get_sysinfo"])
|
||||||
|
|
||||||
|
if not self._modules:
|
||||||
|
await self._initialize_modules()
|
||||||
|
|
||||||
|
await self._modular_update(req)
|
||||||
|
|
||||||
if not self._features:
|
if not self._features:
|
||||||
await self._initialize_features()
|
await self._initialize_features()
|
||||||
|
|
||||||
await self._modular_update(req)
|
|
||||||
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
|
self._set_sys_info(self._last_update["system"]["get_sysinfo"])
|
||||||
|
|
||||||
|
async def _initialize_modules(self):
|
||||||
|
"""Initialize modules not added in init."""
|
||||||
|
|
||||||
async def _initialize_features(self):
|
async def _initialize_features(self):
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
Feature(
|
Feature(
|
||||||
@ -353,29 +342,32 @@ class IotDevice(Device):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for module in self._modules.values():
|
||||||
|
module._initialize_features()
|
||||||
|
for module_feat in module._module_features.values():
|
||||||
|
self._add_feature(module_feat)
|
||||||
|
|
||||||
async def _modular_update(self, req: dict) -> None:
|
async def _modular_update(self, req: dict) -> None:
|
||||||
"""Execute an update query."""
|
"""Execute an update query."""
|
||||||
if self.has_emeter:
|
if self.has_emeter:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"The device has emeter, querying its information along sysinfo"
|
"The device has emeter, querying its information along sysinfo"
|
||||||
)
|
)
|
||||||
self.add_module("emeter", Emeter(self, self.emeter_type))
|
self.add_module(Module.IotEmeter, Emeter(self, self.emeter_type))
|
||||||
|
|
||||||
# TODO: perhaps modules should not have unsupported modules,
|
# TODO: perhaps modules should not have unsupported modules,
|
||||||
# making separate handling for this unnecessary
|
# making separate handling for this unnecessary
|
||||||
if self._supported_modules is None:
|
if self._supported_modules is None:
|
||||||
supported = {}
|
supported = {}
|
||||||
for module in self.modules.values():
|
for module in self._modules.values():
|
||||||
if module.is_supported:
|
if module.is_supported:
|
||||||
supported[module._module] = module
|
supported[module._module] = module
|
||||||
for module_feat in module._module_features.values():
|
|
||||||
self._add_feature(module_feat)
|
|
||||||
|
|
||||||
self._supported_modules = supported
|
self._supported_modules = supported
|
||||||
|
|
||||||
request_list = []
|
request_list = []
|
||||||
est_response_size = 1024 if "system" in req else 0
|
est_response_size = 1024 if "system" in req else 0
|
||||||
for module in self.modules.values():
|
for module in self._modules.values():
|
||||||
if not module.is_supported:
|
if not module.is_supported:
|
||||||
_LOGGER.debug("Module %s not supported, skipping" % module)
|
_LOGGER.debug("Module %s not supported, skipping" % module)
|
||||||
continue
|
continue
|
||||||
@ -454,27 +446,27 @@ class IotDevice(Device):
|
|||||||
@requires_update
|
@requires_update
|
||||||
def time(self) -> datetime:
|
def time(self) -> datetime:
|
||||||
"""Return current time from the device."""
|
"""Return current time from the device."""
|
||||||
return cast(Time, self.modules["time"]).time
|
return self.modules[Module.IotTime].time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requires_update
|
@requires_update
|
||||||
def timezone(self) -> dict:
|
def timezone(self) -> dict:
|
||||||
"""Return the current timezone."""
|
"""Return the current timezone."""
|
||||||
return cast(Time, self.modules["time"]).timezone
|
return self.modules[Module.IotTime].timezone
|
||||||
|
|
||||||
async def get_time(self) -> datetime | None:
|
async def get_time(self) -> datetime | None:
|
||||||
"""Return current time from the device, if available."""
|
"""Return current time from the device, if available."""
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Use `time` property instead, this call will be removed in the future."
|
"Use `time` property instead, this call will be removed in the future."
|
||||||
)
|
)
|
||||||
return await cast(Time, self.modules["time"]).get_time()
|
return await self.modules[Module.IotTime].get_time()
|
||||||
|
|
||||||
async def get_timezone(self) -> dict:
|
async def get_timezone(self) -> dict:
|
||||||
"""Return timezone information."""
|
"""Return timezone information."""
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Use `timezone` property instead, this call will be removed in the future."
|
"Use `timezone` property instead, this call will be removed in the future."
|
||||||
)
|
)
|
||||||
return await cast(Time, self.modules["time"]).get_timezone()
|
return await self.modules[Module.IotTime].get_timezone()
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -555,26 +547,26 @@ class IotDevice(Device):
|
|||||||
def emeter_realtime(self) -> EmeterStatus:
|
def emeter_realtime(self) -> EmeterStatus:
|
||||||
"""Return current energy readings."""
|
"""Return current energy readings."""
|
||||||
self._verify_emeter()
|
self._verify_emeter()
|
||||||
return EmeterStatus(cast(Emeter, self.modules["emeter"]).realtime)
|
return EmeterStatus(self.modules[Module.IotEmeter].realtime)
|
||||||
|
|
||||||
async def get_emeter_realtime(self) -> EmeterStatus:
|
async def get_emeter_realtime(self) -> EmeterStatus:
|
||||||
"""Retrieve current energy readings."""
|
"""Retrieve current energy readings."""
|
||||||
self._verify_emeter()
|
self._verify_emeter()
|
||||||
return EmeterStatus(await cast(Emeter, self.modules["emeter"]).get_realtime())
|
return EmeterStatus(await self.modules[Module.IotEmeter].get_realtime())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requires_update
|
@requires_update
|
||||||
def emeter_today(self) -> float | None:
|
def emeter_today(self) -> float | None:
|
||||||
"""Return today's energy consumption in kWh."""
|
"""Return today's energy consumption in kWh."""
|
||||||
self._verify_emeter()
|
self._verify_emeter()
|
||||||
return cast(Emeter, self.modules["emeter"]).emeter_today
|
return self.modules[Module.IotEmeter].emeter_today
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requires_update
|
@requires_update
|
||||||
def emeter_this_month(self) -> float | None:
|
def emeter_this_month(self) -> float | None:
|
||||||
"""Return this month's energy consumption in kWh."""
|
"""Return this month's energy consumption in kWh."""
|
||||||
self._verify_emeter()
|
self._verify_emeter()
|
||||||
return cast(Emeter, self.modules["emeter"]).emeter_this_month
|
return self.modules[Module.IotEmeter].emeter_this_month
|
||||||
|
|
||||||
async def get_emeter_daily(
|
async def get_emeter_daily(
|
||||||
self, year: int | None = None, month: int | None = None, kwh: bool = True
|
self, year: int | None = None, month: int | None = None, kwh: bool = True
|
||||||
@ -588,7 +580,7 @@ class IotDevice(Device):
|
|||||||
:return: mapping of day of month to value
|
:return: mapping of day of month to value
|
||||||
"""
|
"""
|
||||||
self._verify_emeter()
|
self._verify_emeter()
|
||||||
return await cast(Emeter, self.modules["emeter"]).get_daystat(
|
return await self.modules[Module.IotEmeter].get_daystat(
|
||||||
year=year, month=month, kwh=kwh
|
year=year, month=month, kwh=kwh
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -603,15 +595,13 @@ class IotDevice(Device):
|
|||||||
:return: dict: mapping of month to value
|
:return: dict: mapping of month to value
|
||||||
"""
|
"""
|
||||||
self._verify_emeter()
|
self._verify_emeter()
|
||||||
return await cast(Emeter, self.modules["emeter"]).get_monthstat(
|
return await self.modules[Module.IotEmeter].get_monthstat(year=year, kwh=kwh)
|
||||||
year=year, kwh=kwh
|
|
||||||
)
|
|
||||||
|
|
||||||
@requires_update
|
@requires_update
|
||||||
async def erase_emeter_stats(self) -> dict:
|
async def erase_emeter_stats(self) -> dict:
|
||||||
"""Erase energy meter statistics."""
|
"""Erase energy meter statistics."""
|
||||||
self._verify_emeter()
|
self._verify_emeter()
|
||||||
return await cast(Emeter, self.modules["emeter"]).erase_stats()
|
return await self.modules[Module.IotEmeter].erase_stats()
|
||||||
|
|
||||||
@requires_update
|
@requires_update
|
||||||
async def current_consumption(self) -> float:
|
async def current_consumption(self) -> float:
|
||||||
@ -716,9 +706,3 @@ class IotDevice(Device):
|
|||||||
This should only be used for debugging purposes.
|
This should only be used for debugging purposes.
|
||||||
"""
|
"""
|
||||||
return self._last_update or self._discovery_info
|
return self._last_update or self._discovery_info
|
||||||
|
|
||||||
@property
|
|
||||||
@requires_update
|
|
||||||
def firmware(self) -> Firmware:
|
|
||||||
"""Returns object implementing the firmware handling."""
|
|
||||||
return cast(Firmware, self.modules["cloud"])
|
|
||||||
|
@ -7,11 +7,11 @@ from typing import Any
|
|||||||
|
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..feature import Feature
|
from ..module import Module
|
||||||
from ..protocol import BaseProtocol
|
from ..protocol import BaseProtocol
|
||||||
from .iotdevice import KasaException, requires_update
|
from .iotdevice import KasaException, requires_update
|
||||||
from .iotplug import IotPlug
|
from .iotplug import IotPlug
|
||||||
from .modules import AmbientLight, Motion
|
from .modules import AmbientLight, Light, Motion
|
||||||
|
|
||||||
|
|
||||||
class ButtonAction(Enum):
|
class ButtonAction(Enum):
|
||||||
@ -79,29 +79,15 @@ class IotDimmer(IotPlug):
|
|||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(host=host, config=config, protocol=protocol)
|
super().__init__(host=host, config=config, protocol=protocol)
|
||||||
self._device_type = DeviceType.Dimmer
|
self._device_type = DeviceType.Dimmer
|
||||||
|
|
||||||
|
async def _initialize_modules(self):
|
||||||
|
"""Initialize modules."""
|
||||||
|
await super()._initialize_modules()
|
||||||
# TODO: need to be verified if it's okay to call these on HS220 w/o these
|
# TODO: need to be verified if it's okay to call these on HS220 w/o these
|
||||||
# TODO: need to be figured out what's the best approach to detect support
|
# TODO: need to be figured out what's the best approach to detect support
|
||||||
self.add_module("motion", Motion(self, "smartlife.iot.PIR"))
|
self.add_module(Module.IotMotion, Motion(self, "smartlife.iot.PIR"))
|
||||||
self.add_module("ambient", AmbientLight(self, "smartlife.iot.LAS"))
|
self.add_module(Module.IotAmbientLight, AmbientLight(self, "smartlife.iot.LAS"))
|
||||||
|
self.add_module(Module.Light, Light(self, "light"))
|
||||||
async def _initialize_features(self):
|
|
||||||
await super()._initialize_features()
|
|
||||||
|
|
||||||
if "brightness" in self.sys_info: # pragma: no branch
|
|
||||||
self._add_feature(
|
|
||||||
Feature(
|
|
||||||
device=self,
|
|
||||||
id="brightness",
|
|
||||||
name="Brightness",
|
|
||||||
attribute_getter="brightness",
|
|
||||||
attribute_setter="set_brightness",
|
|
||||||
minimum_value=1,
|
|
||||||
maximum_value=100,
|
|
||||||
unit="%",
|
|
||||||
type=Feature.Type.Number,
|
|
||||||
category=Feature.Category.Primary,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -110,7 +96,7 @@ class IotDimmer(IotPlug):
|
|||||||
|
|
||||||
Will return a range between 0 - 100.
|
Will return a range between 0 - 100.
|
||||||
"""
|
"""
|
||||||
if not self.is_dimmable:
|
if not self._is_dimmable:
|
||||||
raise KasaException("Device is not dimmable.")
|
raise KasaException("Device is not dimmable.")
|
||||||
|
|
||||||
sys_info = self.sys_info
|
sys_info = self.sys_info
|
||||||
@ -123,7 +109,7 @@ class IotDimmer(IotPlug):
|
|||||||
:param int transition: transition duration in milliseconds.
|
:param int transition: transition duration in milliseconds.
|
||||||
Using a transition will cause the dimmer to turn on.
|
Using a transition will cause the dimmer to turn on.
|
||||||
"""
|
"""
|
||||||
if not self.is_dimmable:
|
if not self._is_dimmable:
|
||||||
raise KasaException("Device is not dimmable.")
|
raise KasaException("Device is not dimmable.")
|
||||||
|
|
||||||
if not isinstance(brightness, int):
|
if not isinstance(brightness, int):
|
||||||
@ -232,7 +218,7 @@ class IotDimmer(IotPlug):
|
|||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def is_dimmable(self) -> bool:
|
def _is_dimmable(self) -> bool:
|
||||||
"""Whether the switch supports brightness changes."""
|
"""Whether the switch supports brightness changes."""
|
||||||
sys_info = self.sys_info
|
sys_info = self.sys_info
|
||||||
return "brightness" in sys_info
|
return "brightness" in sys_info
|
||||||
|
@ -4,10 +4,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..effects import EFFECT_MAPPING_V1, EFFECT_NAMES_V1
|
from ..module import Module
|
||||||
from ..protocol import BaseProtocol
|
from ..protocol import BaseProtocol
|
||||||
|
from .effects import EFFECT_NAMES_V1
|
||||||
from .iotbulb import IotBulb
|
from .iotbulb import IotBulb
|
||||||
from .iotdevice import KasaException, requires_update
|
from .iotdevice import KasaException, requires_update
|
||||||
|
from .modules.lighteffect import LightEffect
|
||||||
|
|
||||||
|
|
||||||
class IotLightStrip(IotBulb):
|
class IotLightStrip(IotBulb):
|
||||||
@ -55,6 +57,14 @@ class IotLightStrip(IotBulb):
|
|||||||
super().__init__(host=host, config=config, protocol=protocol)
|
super().__init__(host=host, config=config, protocol=protocol)
|
||||||
self._device_type = DeviceType.LightStrip
|
self._device_type = DeviceType.LightStrip
|
||||||
|
|
||||||
|
async def _initialize_modules(self):
|
||||||
|
"""Initialize modules not added in init."""
|
||||||
|
await super()._initialize_modules()
|
||||||
|
self.add_module(
|
||||||
|
Module.LightEffect,
|
||||||
|
LightEffect(self, "smartlife.iot.lighting_effect"),
|
||||||
|
)
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
def length(self) -> int:
|
def length(self) -> int:
|
||||||
@ -73,6 +83,8 @@ class IotLightStrip(IotBulb):
|
|||||||
'id': '',
|
'id': '',
|
||||||
'name': ''}
|
'name': ''}
|
||||||
"""
|
"""
|
||||||
|
# LightEffectModule returns the current effect name
|
||||||
|
# so return the dict here for backwards compatibility
|
||||||
return self.sys_info["lighting_effect_state"]
|
return self.sys_info["lighting_effect_state"]
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@ -83,6 +95,8 @@ class IotLightStrip(IotBulb):
|
|||||||
Example:
|
Example:
|
||||||
['Aurora', 'Bubbling Cauldron', ...]
|
['Aurora', 'Bubbling Cauldron', ...]
|
||||||
"""
|
"""
|
||||||
|
# LightEffectModule returns effect names along with a LIGHT_EFFECTS_OFF value
|
||||||
|
# so return the original effect names here for backwards compatibility
|
||||||
return EFFECT_NAMES_V1 if self.has_effects else None
|
return EFFECT_NAMES_V1 if self.has_effects else None
|
||||||
|
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -105,15 +119,9 @@ class IotLightStrip(IotBulb):
|
|||||||
:param int brightness: The wanted brightness
|
:param int brightness: The wanted brightness
|
||||||
:param int transition: The wanted transition time
|
:param int transition: The wanted transition time
|
||||||
"""
|
"""
|
||||||
if effect not in EFFECT_MAPPING_V1:
|
await self.modules[Module.LightEffect].set_effect(
|
||||||
raise KasaException(f"The effect {effect} is not a built in effect.")
|
effect, brightness=brightness, transition=transition
|
||||||
effect_dict = EFFECT_MAPPING_V1[effect]
|
)
|
||||||
if brightness is not None:
|
|
||||||
effect_dict["brightness"] = brightness
|
|
||||||
if transition is not None:
|
|
||||||
effect_dict["transition"] = transition
|
|
||||||
|
|
||||||
await self.set_custom_effect(effect_dict)
|
|
||||||
|
|
||||||
@requires_update
|
@requires_update
|
||||||
async def set_custom_effect(
|
async def set_custom_effect(
|
||||||
@ -126,8 +134,4 @@ class IotLightStrip(IotBulb):
|
|||||||
"""
|
"""
|
||||||
if not self.has_effects:
|
if not self.has_effects:
|
||||||
raise KasaException("Bulb does not support effects.")
|
raise KasaException("Bulb does not support effects.")
|
||||||
await self._query_helper(
|
await self.modules[Module.LightEffect].set_custom_effect(effect_dict)
|
||||||
"smartlife.iot.lighting_effect",
|
|
||||||
"set_lighting_effect",
|
|
||||||
effect_dict,
|
|
||||||
)
|
|
||||||
|
@ -43,13 +43,19 @@ class IotModule(Module):
|
|||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
"""Return the module specific raw data from the last update."""
|
"""Return the module specific raw data from the last update."""
|
||||||
if self._module not in self._device._last_update:
|
dev = self._device
|
||||||
|
q = self.query()
|
||||||
|
|
||||||
|
if not q:
|
||||||
|
return dev.sys_info
|
||||||
|
|
||||||
|
if self._module not in dev._last_update:
|
||||||
raise KasaException(
|
raise KasaException(
|
||||||
f"You need to call update() prior accessing module data"
|
f"You need to call update() prior accessing module data"
|
||||||
f" for '{self._module}'"
|
f" for '{self._module}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._device._last_update[self._module]
|
return dev._last_update[self._module]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_supported(self) -> bool:
|
def is_supported(self) -> bool:
|
||||||
|
@ -6,10 +6,10 @@ import logging
|
|||||||
|
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..feature import Feature
|
from ..module import Module
|
||||||
from ..protocol import BaseProtocol
|
from ..protocol import BaseProtocol
|
||||||
from .iotdevice import IotDevice, requires_update
|
from .iotdevice import IotDevice, requires_update
|
||||||
from .modules import Antitheft, Cloud, Schedule, Time, Usage
|
from .modules import Antitheft, Cloud, Led, Schedule, Time, Usage
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -53,26 +53,16 @@ class IotPlug(IotDevice):
|
|||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(host=host, config=config, protocol=protocol)
|
super().__init__(host=host, config=config, protocol=protocol)
|
||||||
self._device_type = DeviceType.Plug
|
self._device_type = DeviceType.Plug
|
||||||
self.add_module("schedule", Schedule(self, "schedule"))
|
|
||||||
self.add_module("usage", Usage(self, "schedule"))
|
|
||||||
self.add_module("antitheft", Antitheft(self, "anti_theft"))
|
|
||||||
self.add_module("time", Time(self, "time"))
|
|
||||||
self.add_module("cloud", Cloud(self, "cnCloud"))
|
|
||||||
|
|
||||||
async def _initialize_features(self):
|
async def _initialize_modules(self):
|
||||||
await super()._initialize_features()
|
"""Initialize modules."""
|
||||||
|
await super()._initialize_modules()
|
||||||
self._add_feature(
|
self.add_module(Module.IotSchedule, Schedule(self, "schedule"))
|
||||||
Feature(
|
self.add_module(Module.IotUsage, Usage(self, "schedule"))
|
||||||
device=self,
|
self.add_module(Module.IotAntitheft, Antitheft(self, "anti_theft"))
|
||||||
id="led",
|
self.add_module(Module.IotTime, Time(self, "time"))
|
||||||
name="LED",
|
self.add_module(Module.IotCloud, Cloud(self, "cnCloud"))
|
||||||
icon="mdi:led-{state}",
|
self.add_module(Module.Led, Led(self, "system"))
|
||||||
attribute_getter="led",
|
|
||||||
attribute_setter="set_led",
|
|
||||||
type=Feature.Type.Switch,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -93,14 +83,11 @@ class IotPlug(IotDevice):
|
|||||||
@requires_update
|
@requires_update
|
||||||
def led(self) -> bool:
|
def led(self) -> bool:
|
||||||
"""Return the state of the led."""
|
"""Return the state of the led."""
|
||||||
sys_info = self.sys_info
|
return self.modules[Module.Led].led
|
||||||
return bool(1 - sys_info["led_off"])
|
|
||||||
|
|
||||||
async def set_led(self, state: bool):
|
async def set_led(self, state: bool):
|
||||||
"""Set the state of the led (night mode)."""
|
"""Set the state of the led (night mode)."""
|
||||||
return await self._query_helper(
|
return await self.modules[Module.Led].set_led(state)
|
||||||
"system", "set_led_off", {"off": int(not state)}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IotWallSwitch(IotPlug):
|
class IotWallSwitch(IotPlug):
|
||||||
|
@ -10,6 +10,7 @@ from typing import Any
|
|||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..exceptions import KasaException
|
from ..exceptions import KasaException
|
||||||
|
from ..module import Module
|
||||||
from ..protocol import BaseProtocol
|
from ..protocol import BaseProtocol
|
||||||
from .iotdevice import (
|
from .iotdevice import (
|
||||||
EmeterStatus,
|
EmeterStatus,
|
||||||
@ -95,11 +96,11 @@ class IotStrip(IotDevice):
|
|||||||
super().__init__(host=host, config=config, protocol=protocol)
|
super().__init__(host=host, config=config, protocol=protocol)
|
||||||
self.emeter_type = "emeter"
|
self.emeter_type = "emeter"
|
||||||
self._device_type = DeviceType.Strip
|
self._device_type = DeviceType.Strip
|
||||||
self.add_module("antitheft", Antitheft(self, "anti_theft"))
|
self.add_module(Module.IotAntitheft, Antitheft(self, "anti_theft"))
|
||||||
self.add_module("schedule", Schedule(self, "schedule"))
|
self.add_module(Module.IotSchedule, Schedule(self, "schedule"))
|
||||||
self.add_module("usage", Usage(self, "schedule"))
|
self.add_module(Module.IotUsage, Usage(self, "schedule"))
|
||||||
self.add_module("time", Time(self, "time"))
|
self.add_module(Module.IotTime, Time(self, "time"))
|
||||||
self.add_module("countdown", Countdown(self, "countdown"))
|
self.add_module(Module.IotCountdown, Countdown(self, "countdown"))
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
@ -253,8 +254,11 @@ class IotStripPlug(IotPlug):
|
|||||||
self._last_update = parent._last_update
|
self._last_update = parent._last_update
|
||||||
self._set_sys_info(parent.sys_info)
|
self._set_sys_info(parent.sys_info)
|
||||||
self._device_type = DeviceType.StripSocket
|
self._device_type = DeviceType.StripSocket
|
||||||
self._modules = {}
|
|
||||||
self.protocol = parent.protocol # Must use the same connection as the parent
|
self.protocol = parent.protocol # Must use the same connection as the parent
|
||||||
|
|
||||||
|
async def _initialize_modules(self):
|
||||||
|
"""Initialize modules not added in init."""
|
||||||
|
await super()._initialize_modules()
|
||||||
self.add_module("time", Time(self, "time"))
|
self.add_module("time", Time(self, "time"))
|
||||||
|
|
||||||
async def update(self, update_children: bool = True):
|
async def update(self, update_children: bool = True):
|
||||||
|
@ -5,6 +5,9 @@ from .antitheft import Antitheft
|
|||||||
from .cloud import Cloud
|
from .cloud import Cloud
|
||||||
from .countdown import Countdown
|
from .countdown import Countdown
|
||||||
from .emeter import Emeter
|
from .emeter import Emeter
|
||||||
|
from .led import Led
|
||||||
|
from .light import Light
|
||||||
|
from .lighteffect import LightEffect
|
||||||
from .motion import Motion
|
from .motion import Motion
|
||||||
from .rulemodule import Rule, RuleModule
|
from .rulemodule import Rule, RuleModule
|
||||||
from .schedule import Schedule
|
from .schedule import Schedule
|
||||||
@ -17,6 +20,9 @@ __all__ = [
|
|||||||
"Cloud",
|
"Cloud",
|
||||||
"Countdown",
|
"Countdown",
|
||||||
"Emeter",
|
"Emeter",
|
||||||
|
"Led",
|
||||||
|
"Light",
|
||||||
|
"LightEffect",
|
||||||
"Motion",
|
"Motion",
|
||||||
"Rule",
|
"Rule",
|
||||||
"RuleModule",
|
"RuleModule",
|
||||||
|
@ -4,19 +4,22 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import Optional
|
from typing import Callable, Coroutine, Optional
|
||||||
|
|
||||||
from pydantic.v1 import BaseModel, Field, validator
|
from pydantic.v1 import BaseModel, Field, validator
|
||||||
|
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
from ...firmware import (
|
from ...interfaces.firmware import (
|
||||||
Firmware,
|
Firmware,
|
||||||
UpdateResult,
|
UpdateResult,
|
||||||
)
|
)
|
||||||
from ...firmware import (
|
from ...interfaces.firmware import (
|
||||||
FirmwareUpdate as FirmwareUpdateInterface,
|
FirmwareDownloadState as FirmwareDownloadStateInterface,
|
||||||
)
|
)
|
||||||
from ..iotmodule import IotModule, merge
|
from ...interfaces.firmware import (
|
||||||
|
FirmwareUpdateInfo as FirmwareUpdateInfoInterface,
|
||||||
|
)
|
||||||
|
from ..iotmodule import IotModule
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -89,8 +92,11 @@ class Cloud(IotModule, Firmware):
|
|||||||
|
|
||||||
# TODO: this is problematic, as it will fail the whole query on some
|
# TODO: this is problematic, as it will fail the whole query on some
|
||||||
# devices if they are not connected to the internet
|
# devices if they are not connected to the internet
|
||||||
if self._module in self._device._last_update and self.is_connected:
|
|
||||||
req = merge(req, self.get_available_firmwares())
|
# The following causes a recursion error as self.is_connected
|
||||||
|
# accesses self.data which calls query. Also get_available_firmwares is async
|
||||||
|
# if self._module in self._device._last_update and self.is_connected:
|
||||||
|
# req = merge(req, self.get_available_firmwares())
|
||||||
|
|
||||||
return req
|
return req
|
||||||
|
|
||||||
@ -130,7 +136,12 @@ class Cloud(IotModule, Firmware):
|
|||||||
"""Disconnect from the cloud."""
|
"""Disconnect from the cloud."""
|
||||||
return await self.call("unbind")
|
return await self.call("unbind")
|
||||||
|
|
||||||
async def update_firmware(self, *, progress_cb=None) -> UpdateResult:
|
async def update_firmware(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
progress_cb: Callable[[FirmwareDownloadStateInterface], Coroutine]
|
||||||
|
| None = None,
|
||||||
|
) -> UpdateResult:
|
||||||
"""Perform firmware update."""
|
"""Perform firmware update."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
i = 0
|
i = 0
|
||||||
@ -144,11 +155,16 @@ class Cloud(IotModule, Firmware):
|
|||||||
|
|
||||||
return UpdateResult("")
|
return UpdateResult("")
|
||||||
|
|
||||||
async def check_for_updates(self) -> FirmwareUpdateInterface:
|
async def check_for_updates(self) -> FirmwareUpdateInfoInterface:
|
||||||
|
"""Return firmware update information."""
|
||||||
|
# TODO: naming of the common firmware API methods
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def get_update_state(self) -> FirmwareUpdateInfoInterface:
|
||||||
"""Return firmware update information."""
|
"""Return firmware update information."""
|
||||||
fw = await self.get_firmware_update()
|
fw = await self.get_firmware_update()
|
||||||
|
|
||||||
return FirmwareUpdateInterface(
|
return FirmwareUpdateInfoInterface(
|
||||||
update_available=fw.update_available,
|
update_available=fw.update_available,
|
||||||
current_version=self._device.hw_info.get("sw_ver"),
|
current_version=self._device.hw_info.get("sw_ver"),
|
||||||
available_version=fw.version,
|
available_version=fw.version,
|
||||||
|
32
kasa/iot/modules/led.py
Normal file
32
kasa/iot/modules/led.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""Module for led controls."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...interfaces.led import Led as LedInterface
|
||||||
|
from ..iotmodule import IotModule
|
||||||
|
|
||||||
|
|
||||||
|
class Led(IotModule, LedInterface):
|
||||||
|
"""Implementation of led controls."""
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mode(self):
|
||||||
|
"""LED mode setting.
|
||||||
|
|
||||||
|
"always", "never"
|
||||||
|
"""
|
||||||
|
return "always" if self.led else "never"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def led(self) -> bool:
|
||||||
|
"""Return the state of the led."""
|
||||||
|
sys_info = self.data
|
||||||
|
return bool(1 - sys_info["led_off"])
|
||||||
|
|
||||||
|
async def set_led(self, state: bool):
|
||||||
|
"""Set the state of the led (night mode)."""
|
||||||
|
return await self.call("set_led_off", {"off": int(not state)})
|
200
kasa/iot/modules/light.py
Normal file
200
kasa/iot/modules/light.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
"""Implementation of brightness module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
|
from ...device_type import DeviceType
|
||||||
|
from ...exceptions import KasaException
|
||||||
|
from ...feature import Feature
|
||||||
|
from ...interfaces.light import HSV, ColorTempRange
|
||||||
|
from ...interfaces.light import Light as LightInterface
|
||||||
|
from ..iotmodule import IotModule
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..iotbulb import IotBulb
|
||||||
|
from ..iotdimmer import IotDimmer
|
||||||
|
|
||||||
|
|
||||||
|
BRIGHTNESS_MIN = 0
|
||||||
|
BRIGHTNESS_MAX = 100
|
||||||
|
|
||||||
|
|
||||||
|
class Light(IotModule, LightInterface):
|
||||||
|
"""Implementation of brightness module."""
|
||||||
|
|
||||||
|
_device: IotBulb | IotDimmer
|
||||||
|
|
||||||
|
def _initialize_features(self):
|
||||||
|
"""Initialize features."""
|
||||||
|
super()._initialize_features()
|
||||||
|
device = self._device
|
||||||
|
|
||||||
|
if self._device.is_dimmable:
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device,
|
||||||
|
id="brightness",
|
||||||
|
name="Brightness",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="brightness",
|
||||||
|
attribute_setter="set_brightness",
|
||||||
|
minimum_value=BRIGHTNESS_MIN,
|
||||||
|
maximum_value=BRIGHTNESS_MAX,
|
||||||
|
type=Feature.Type.Number,
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self._device.is_variable_color_temp:
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device=device,
|
||||||
|
id="color_temperature",
|
||||||
|
name="Color temperature",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="color_temp",
|
||||||
|
attribute_setter="set_color_temp",
|
||||||
|
range_getter="valid_temperature_range",
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Type.Number,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if self._device.is_color:
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device=device,
|
||||||
|
id="hsv",
|
||||||
|
name="HSV",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="hsv",
|
||||||
|
attribute_setter="set_hsv",
|
||||||
|
# TODO proper type for setting hsv
|
||||||
|
type=Feature.Type.Unknown,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
# Brightness is contained in the main device info response.
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _get_bulb_device(self) -> IotBulb | None:
|
||||||
|
"""For type checker this gets an IotBulb.
|
||||||
|
|
||||||
|
IotDimmer is not a subclass of IotBulb and using isinstance
|
||||||
|
here at runtime would create a circular import.
|
||||||
|
"""
|
||||||
|
if self._device.device_type in {DeviceType.Bulb, DeviceType.LightStrip}:
|
||||||
|
return cast("IotBulb", self._device)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
def is_dimmable(self) -> int:
|
||||||
|
"""Whether the bulb supports brightness changes."""
|
||||||
|
return self._device._is_dimmable
|
||||||
|
|
||||||
|
@property # type: ignore
|
||||||
|
def brightness(self) -> int:
|
||||||
|
"""Return the current brightness in percentage."""
|
||||||
|
return self._device.brightness
|
||||||
|
|
||||||
|
async def set_brightness(
|
||||||
|
self, brightness: int, *, transition: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""Set the brightness in percentage.
|
||||||
|
|
||||||
|
:param int brightness: brightness in percent
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
return await self._device.set_brightness(brightness, transition=transition)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_color(self) -> bool:
|
||||||
|
"""Whether the light supports color changes."""
|
||||||
|
if (bulb := self._get_bulb_device()) is None:
|
||||||
|
return False
|
||||||
|
return bulb._is_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_variable_color_temp(self) -> bool:
|
||||||
|
"""Whether the bulb supports color temperature changes."""
|
||||||
|
if (bulb := self._get_bulb_device()) is None:
|
||||||
|
return False
|
||||||
|
return bulb._is_variable_color_temp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_effects(self) -> bool:
|
||||||
|
"""Return True if the device supports effects."""
|
||||||
|
if (bulb := self._get_bulb_device()) is None:
|
||||||
|
return False
|
||||||
|
return bulb.has_effects
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hsv(self) -> HSV:
|
||||||
|
"""Return the current HSV state of the bulb.
|
||||||
|
|
||||||
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
|
"""
|
||||||
|
if (bulb := self._get_bulb_device()) is None or not bulb._is_color:
|
||||||
|
raise KasaException("Light does not support color.")
|
||||||
|
return bulb.hsv
|
||||||
|
|
||||||
|
async def set_hsv(
|
||||||
|
self,
|
||||||
|
hue: int,
|
||||||
|
saturation: int,
|
||||||
|
value: int | None = None,
|
||||||
|
*,
|
||||||
|
transition: int | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Set new HSV.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int hue: hue in degrees
|
||||||
|
:param int saturation: saturation in percentage [0,100]
|
||||||
|
:param int value: value in percentage [0, 100]
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
if (bulb := self._get_bulb_device()) is None or not bulb._is_color:
|
||||||
|
raise KasaException("Light does not support color.")
|
||||||
|
return await bulb.set_hsv(hue, saturation, value, transition=transition)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid_temperature_range(self) -> ColorTempRange:
|
||||||
|
"""Return the device-specific white temperature range (in Kelvin).
|
||||||
|
|
||||||
|
:return: White temperature range in Kelvin (minimum, maximum)
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
bulb := self._get_bulb_device()
|
||||||
|
) is None or not bulb._is_variable_color_temp:
|
||||||
|
raise KasaException("Light does not support colortemp.")
|
||||||
|
return bulb.valid_temperature_range
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self) -> int:
|
||||||
|
"""Whether the bulb supports color temperature changes."""
|
||||||
|
if (
|
||||||
|
bulb := self._get_bulb_device()
|
||||||
|
) is None or not bulb._is_variable_color_temp:
|
||||||
|
raise KasaException("Light does not support colortemp.")
|
||||||
|
return bulb.color_temp
|
||||||
|
|
||||||
|
async def set_color_temp(
|
||||||
|
self, temp: int, *, brightness=None, transition: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""Set the color temperature of the device in kelvin.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int temp: The new color temperature, in Kelvin
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
bulb := self._get_bulb_device()
|
||||||
|
) is None or not bulb._is_variable_color_temp:
|
||||||
|
raise KasaException("Light does not support colortemp.")
|
||||||
|
return await bulb.set_color_temp(
|
||||||
|
temp, brightness=brightness, transition=transition
|
||||||
|
)
|
97
kasa/iot/modules/lighteffect.py
Normal file
97
kasa/iot/modules/lighteffect.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
"""Module for light effects."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...interfaces.lighteffect import LightEffect as LightEffectInterface
|
||||||
|
from ..effects import EFFECT_MAPPING_V1, EFFECT_NAMES_V1
|
||||||
|
from ..iotmodule import IotModule
|
||||||
|
|
||||||
|
|
||||||
|
class LightEffect(IotModule, LightEffectInterface):
|
||||||
|
"""Implementation of dynamic light effects."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect(self) -> str:
|
||||||
|
"""Return effect state.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
{'brightness': 50,
|
||||||
|
'custom': 0,
|
||||||
|
'enable': 0,
|
||||||
|
'id': '',
|
||||||
|
'name': ''}
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
(state := self.data.get("lighting_effect_state"))
|
||||||
|
and state.get("enable")
|
||||||
|
and (name := state.get("name"))
|
||||||
|
and name in EFFECT_NAMES_V1
|
||||||
|
):
|
||||||
|
return name
|
||||||
|
return self.LIGHT_EFFECTS_OFF
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effect_list(self) -> list[str]:
|
||||||
|
"""Return built-in effects list.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
['Aurora', 'Bubbling Cauldron', ...]
|
||||||
|
"""
|
||||||
|
effect_list = [self.LIGHT_EFFECTS_OFF]
|
||||||
|
effect_list.extend(EFFECT_NAMES_V1)
|
||||||
|
return effect_list
|
||||||
|
|
||||||
|
async def set_effect(
|
||||||
|
self,
|
||||||
|
effect: str,
|
||||||
|
*,
|
||||||
|
brightness: int | None = None,
|
||||||
|
transition: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Set an effect on the device.
|
||||||
|
|
||||||
|
If brightness or transition is defined,
|
||||||
|
its value will be used instead of the effect-specific default.
|
||||||
|
|
||||||
|
See :meth:`effect_list` for available effects,
|
||||||
|
or use :meth:`set_custom_effect` for custom effects.
|
||||||
|
|
||||||
|
:param str effect: The effect to set
|
||||||
|
:param int brightness: The wanted brightness
|
||||||
|
:param int transition: The wanted transition time
|
||||||
|
"""
|
||||||
|
if effect == self.LIGHT_EFFECTS_OFF:
|
||||||
|
effect_dict = dict(self.data["lighting_effect_state"])
|
||||||
|
effect_dict["enable"] = 0
|
||||||
|
elif effect not in EFFECT_MAPPING_V1:
|
||||||
|
raise ValueError(f"The effect {effect} is not a built in effect.")
|
||||||
|
else:
|
||||||
|
effect_dict = EFFECT_MAPPING_V1[effect]
|
||||||
|
if brightness is not None:
|
||||||
|
effect_dict["brightness"] = brightness
|
||||||
|
if transition is not None:
|
||||||
|
effect_dict["transition"] = transition
|
||||||
|
|
||||||
|
await self.set_custom_effect(effect_dict)
|
||||||
|
|
||||||
|
async def set_custom_effect(
|
||||||
|
self,
|
||||||
|
effect_dict: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Set a custom effect on the device.
|
||||||
|
|
||||||
|
:param str effect_dict: The custom effect dict to set
|
||||||
|
"""
|
||||||
|
return await self.call(
|
||||||
|
"set_lighting_effect",
|
||||||
|
effect_dict,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_custom_effects(self) -> bool:
|
||||||
|
"""Return True if the device supports setting custom effects."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def query(self):
|
||||||
|
"""Return the base query."""
|
||||||
|
return {}
|
@ -6,14 +6,19 @@ import logging
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
Final,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .exceptions import KasaException
|
from .exceptions import KasaException
|
||||||
from .feature import Feature
|
from .feature import Feature
|
||||||
|
from .modulemapping import ModuleName
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from . import interfaces
|
||||||
from .device import Device
|
from .device import Device
|
||||||
|
from .iot import modules as iot
|
||||||
|
from .smart import modules as smart
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -27,6 +32,59 @@ class Module(ABC):
|
|||||||
executed during the regular update cycle.
|
executed during the regular update cycle.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Common Modules
|
||||||
|
LightEffect: Final[ModuleName[interfaces.LightEffect]] = ModuleName("LightEffect")
|
||||||
|
Led: Final[ModuleName[interfaces.Led]] = ModuleName("Led")
|
||||||
|
Light: Final[ModuleName[interfaces.Light]] = ModuleName("Light")
|
||||||
|
Firmware: Final[ModuleName[interfaces.Firmware]] = ModuleName("Firmware")
|
||||||
|
|
||||||
|
# IOT only Modules
|
||||||
|
IotAmbientLight: Final[ModuleName[iot.AmbientLight]] = ModuleName("ambient")
|
||||||
|
IotAntitheft: Final[ModuleName[iot.Antitheft]] = ModuleName("anti_theft")
|
||||||
|
IotCountdown: Final[ModuleName[iot.Countdown]] = ModuleName("countdown")
|
||||||
|
IotEmeter: Final[ModuleName[iot.Emeter]] = ModuleName("emeter")
|
||||||
|
IotMotion: Final[ModuleName[iot.Motion]] = ModuleName("motion")
|
||||||
|
IotSchedule: Final[ModuleName[iot.Schedule]] = ModuleName("schedule")
|
||||||
|
IotUsage: Final[ModuleName[iot.Usage]] = ModuleName("usage")
|
||||||
|
IotCloud: Final[ModuleName[iot.Cloud]] = ModuleName("cloud")
|
||||||
|
IotTime: Final[ModuleName[iot.Time]] = ModuleName("time")
|
||||||
|
|
||||||
|
# SMART only Modules
|
||||||
|
Alarm: Final[ModuleName[smart.Alarm]] = ModuleName("Alarm")
|
||||||
|
AutoOff: Final[ModuleName[smart.AutoOff]] = ModuleName("AutoOff")
|
||||||
|
BatterySensor: Final[ModuleName[smart.BatterySensor]] = ModuleName("BatterySensor")
|
||||||
|
Brightness: Final[ModuleName[smart.Brightness]] = ModuleName("Brightness")
|
||||||
|
ChildDevice: Final[ModuleName[smart.ChildDevice]] = ModuleName("ChildDevice")
|
||||||
|
Cloud: Final[ModuleName[smart.Cloud]] = ModuleName("Cloud")
|
||||||
|
Color: Final[ModuleName[smart.Color]] = ModuleName("Color")
|
||||||
|
ColorTemperature: Final[ModuleName[smart.ColorTemperature]] = ModuleName(
|
||||||
|
"ColorTemperature"
|
||||||
|
)
|
||||||
|
ContactSensor: Final[ModuleName[smart.ContactSensor]] = ModuleName("ContactSensor")
|
||||||
|
DeviceModule: Final[ModuleName[smart.DeviceModule]] = ModuleName("DeviceModule")
|
||||||
|
Energy: Final[ModuleName[smart.Energy]] = ModuleName("Energy")
|
||||||
|
Fan: Final[ModuleName[smart.Fan]] = ModuleName("Fan")
|
||||||
|
FrostProtection: Final[ModuleName[smart.FrostProtection]] = ModuleName(
|
||||||
|
"FrostProtection"
|
||||||
|
)
|
||||||
|
HumiditySensor: Final[ModuleName[smart.HumiditySensor]] = ModuleName(
|
||||||
|
"HumiditySensor"
|
||||||
|
)
|
||||||
|
LightTransition: Final[ModuleName[smart.LightTransition]] = ModuleName(
|
||||||
|
"LightTransition"
|
||||||
|
)
|
||||||
|
ReportMode: Final[ModuleName[smart.ReportMode]] = ModuleName("ReportMode")
|
||||||
|
TemperatureSensor: Final[ModuleName[smart.TemperatureSensor]] = ModuleName(
|
||||||
|
"TemperatureSensor"
|
||||||
|
)
|
||||||
|
TemperatureControl: Final[ModuleName[smart.TemperatureControl]] = ModuleName(
|
||||||
|
"TemperatureControl"
|
||||||
|
)
|
||||||
|
Time: Final[ModuleName[smart.Time]] = ModuleName("Time")
|
||||||
|
WaterleakSensor: Final[ModuleName[smart.WaterleakSensor]] = ModuleName(
|
||||||
|
"WaterleakSensor"
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, device: Device, module: str):
|
def __init__(self, device: Device, module: str):
|
||||||
self._device = device
|
self._device = device
|
||||||
self._module = module
|
self._module = module
|
||||||
|
25
kasa/modulemapping.py
Normal file
25
kasa/modulemapping.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""Module for Implementation for ModuleMapping and ModuleName types.
|
||||||
|
|
||||||
|
Custom dict for getting typed modules from the module dict.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .module import Module
|
||||||
|
|
||||||
|
_ModuleT = TypeVar("_ModuleT", bound="Module")
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleName(str, Generic[_ModuleT]):
|
||||||
|
"""Generic Module name type.
|
||||||
|
|
||||||
|
At runtime this is a generic subclass of str.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
||||||
|
ModuleMapping = dict
|
96
kasa/modulemapping.pyi
Normal file
96
kasa/modulemapping.pyi
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
"""Typing stub file for ModuleMapping."""
|
||||||
|
|
||||||
|
from abc import ABCMeta
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Generic, TypeVar, overload
|
||||||
|
|
||||||
|
from .module import Module
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ModuleMapping",
|
||||||
|
"ModuleName",
|
||||||
|
]
|
||||||
|
|
||||||
|
_ModuleT = TypeVar("_ModuleT", bound=Module, covariant=True)
|
||||||
|
_ModuleBaseT = TypeVar("_ModuleBaseT", bound=Module, covariant=True)
|
||||||
|
|
||||||
|
class ModuleName(Generic[_ModuleT]):
|
||||||
|
"""Class for typed Module names. At runtime delegated to str."""
|
||||||
|
|
||||||
|
def __init__(self, value: str, /) -> None: ...
|
||||||
|
def __len__(self) -> int: ...
|
||||||
|
def __hash__(self) -> int: ...
|
||||||
|
def __eq__(self, other: object) -> bool: ...
|
||||||
|
def __getitem__(self, index: int) -> str: ...
|
||||||
|
|
||||||
|
class ModuleMapping(
|
||||||
|
Mapping[ModuleName[_ModuleBaseT] | str, _ModuleBaseT], metaclass=ABCMeta
|
||||||
|
):
|
||||||
|
"""Custom dict type to provide better value type hints for Module key types."""
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, key: ModuleName[_ModuleT], /) -> _ModuleT: ...
|
||||||
|
@overload
|
||||||
|
def __getitem__(self, key: str, /) -> _ModuleBaseT: ...
|
||||||
|
@overload
|
||||||
|
def __getitem__(
|
||||||
|
self, key: ModuleName[_ModuleT] | str, /
|
||||||
|
) -> _ModuleT | _ModuleBaseT: ...
|
||||||
|
@overload # type: ignore[override]
|
||||||
|
def get(self, key: ModuleName[_ModuleT], /) -> _ModuleT | None: ...
|
||||||
|
@overload
|
||||||
|
def get(self, key: str, /) -> _ModuleBaseT | None: ...
|
||||||
|
@overload
|
||||||
|
def get(
|
||||||
|
self, key: ModuleName[_ModuleT] | str, /
|
||||||
|
) -> _ModuleT | _ModuleBaseT | None: ...
|
||||||
|
|
||||||
|
def _test_module_mapping_typing() -> None:
|
||||||
|
"""Test ModuleMapping overloads work as intended.
|
||||||
|
|
||||||
|
This is tested during the mypy run and needs to be in this file.
|
||||||
|
"""
|
||||||
|
from typing import Any, NewType, cast
|
||||||
|
|
||||||
|
from typing_extensions import assert_type
|
||||||
|
|
||||||
|
from .iot.iotmodule import IotModule
|
||||||
|
from .module import Module
|
||||||
|
from .smart.smartmodule import SmartModule
|
||||||
|
|
||||||
|
NewCommonModule = NewType("NewCommonModule", Module)
|
||||||
|
NewIotModule = NewType("NewIotModule", IotModule)
|
||||||
|
NewSmartModule = NewType("NewSmartModule", SmartModule)
|
||||||
|
NotModule = NewType("NotModule", list)
|
||||||
|
|
||||||
|
NEW_COMMON_MODULE: ModuleName[NewCommonModule] = ModuleName("NewCommonModule")
|
||||||
|
NEW_IOT_MODULE: ModuleName[NewIotModule] = ModuleName("NewIotModule")
|
||||||
|
NEW_SMART_MODULE: ModuleName[NewSmartModule] = ModuleName("NewSmartModule")
|
||||||
|
|
||||||
|
# TODO Enable --warn-unused-ignores
|
||||||
|
NOT_MODULE: ModuleName[NotModule] = ModuleName("NotModule") # type: ignore[type-var] # noqa: F841
|
||||||
|
NOT_MODULE_2 = ModuleName[NotModule]("NotModule2") # type: ignore[type-var] # noqa: F841
|
||||||
|
|
||||||
|
device_modules: ModuleMapping[Module] = cast(ModuleMapping[Module], {})
|
||||||
|
assert_type(device_modules[NEW_COMMON_MODULE], NewCommonModule)
|
||||||
|
assert_type(device_modules[NEW_IOT_MODULE], NewIotModule)
|
||||||
|
assert_type(device_modules[NEW_SMART_MODULE], NewSmartModule)
|
||||||
|
assert_type(device_modules["foobar"], Module)
|
||||||
|
assert_type(device_modules[3], Any) # type: ignore[call-overload]
|
||||||
|
|
||||||
|
assert_type(device_modules.get(NEW_COMMON_MODULE), NewCommonModule | None)
|
||||||
|
assert_type(device_modules.get(NEW_IOT_MODULE), NewIotModule | None)
|
||||||
|
assert_type(device_modules.get(NEW_SMART_MODULE), NewSmartModule | None)
|
||||||
|
assert_type(device_modules.get(NEW_COMMON_MODULE, default=[1, 2]), Any) # type: ignore[call-overload]
|
||||||
|
|
||||||
|
iot_modules: ModuleMapping[IotModule] = cast(ModuleMapping[IotModule], {})
|
||||||
|
smart_modules: ModuleMapping[SmartModule] = cast(ModuleMapping[SmartModule], {})
|
||||||
|
|
||||||
|
assert_type(smart_modules["foobar"], SmartModule)
|
||||||
|
assert_type(iot_modules["foobar"], IotModule)
|
||||||
|
|
||||||
|
# Test for covariance
|
||||||
|
device_modules_2: ModuleMapping[Module] = iot_modules # noqa: F841
|
||||||
|
device_modules_3: ModuleMapping[Module] = smart_modules # noqa: F841
|
||||||
|
NEW_MODULE: ModuleName[Module] = NEW_SMART_MODULE # noqa: F841
|
||||||
|
NEW_MODULE_2: ModuleName[Module] = NEW_IOT_MODULE # noqa: F841
|
12
kasa/plug.py
12
kasa/plug.py
@ -1,12 +0,0 @@
|
|||||||
"""Module for a TAPO Plug."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from abc import ABC
|
|
||||||
|
|
||||||
from .device import Device
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Plug(Device, ABC):
|
|
||||||
"""Base class to represent a Plug."""
|
|
@ -1,49 +1,53 @@
|
|||||||
"""Modules for SMART devices."""
|
"""Modules for SMART devices."""
|
||||||
|
|
||||||
from .alarmmodule import AlarmModule
|
from .alarm import Alarm
|
||||||
from .autooffmodule import AutoOffModule
|
from .autooff import AutoOff
|
||||||
from .battery import BatterySensor
|
from .batterysensor import BatterySensor
|
||||||
from .brightness import Brightness
|
from .brightness import Brightness
|
||||||
from .childdevicemodule import ChildDeviceModule
|
from .childdevice import ChildDevice
|
||||||
from .cloudmodule import CloudModule
|
from .cloud import Cloud
|
||||||
from .colormodule import ColorModule
|
from .color import Color
|
||||||
from .colortemp import ColorTemperatureModule
|
from .colortemperature import ColorTemperature
|
||||||
|
from .contactsensor import ContactSensor
|
||||||
from .devicemodule import DeviceModule
|
from .devicemodule import DeviceModule
|
||||||
from .energymodule import EnergyModule
|
from .energy import Energy
|
||||||
from .fanmodule import FanModule
|
from .fan import Fan
|
||||||
from .firmware import Firmware
|
from .firmware import Firmware
|
||||||
from .frostprotection import FrostProtectionModule
|
from .frostprotection import FrostProtection
|
||||||
from .humidity import HumiditySensor
|
from .humiditysensor import HumiditySensor
|
||||||
from .ledmodule import LedModule
|
from .led import Led
|
||||||
from .lighteffectmodule import LightEffectModule
|
from .light import Light
|
||||||
from .lighttransitionmodule import LightTransitionModule
|
from .lighteffect import LightEffect
|
||||||
from .reportmodule import ReportModule
|
from .lighttransition import LightTransition
|
||||||
from .temperature import TemperatureSensor
|
from .reportmode import ReportMode
|
||||||
from .temperaturecontrol import TemperatureControl
|
from .temperaturecontrol import TemperatureControl
|
||||||
from .timemodule import TimeModule
|
from .temperaturesensor import TemperatureSensor
|
||||||
from .waterleak import WaterleakSensor
|
from .time import Time
|
||||||
|
from .waterleaksensor import WaterleakSensor
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AlarmModule",
|
"Alarm",
|
||||||
"TimeModule",
|
"Time",
|
||||||
"EnergyModule",
|
"Energy",
|
||||||
"DeviceModule",
|
"DeviceModule",
|
||||||
"ChildDeviceModule",
|
"ChildDevice",
|
||||||
"BatterySensor",
|
"BatterySensor",
|
||||||
"HumiditySensor",
|
"HumiditySensor",
|
||||||
"TemperatureSensor",
|
"TemperatureSensor",
|
||||||
"TemperatureControl",
|
"TemperatureControl",
|
||||||
"ReportModule",
|
"ReportMode",
|
||||||
"AutoOffModule",
|
"AutoOff",
|
||||||
"LedModule",
|
"Led",
|
||||||
"Brightness",
|
"Brightness",
|
||||||
"FanModule",
|
"Fan",
|
||||||
"Firmware",
|
"Firmware",
|
||||||
"CloudModule",
|
"Cloud",
|
||||||
"LightEffectModule",
|
"Light",
|
||||||
"LightTransitionModule",
|
"LightEffect",
|
||||||
"ColorTemperatureModule",
|
"LightTransition",
|
||||||
"ColorModule",
|
"ColorTemperature",
|
||||||
|
"Color",
|
||||||
"WaterleakSensor",
|
"WaterleakSensor",
|
||||||
"FrostProtectionModule",
|
"ContactSensor",
|
||||||
|
"FrostProtection",
|
||||||
]
|
]
|
||||||
|
@ -6,7 +6,7 @@ from ...feature import Feature
|
|||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
class AlarmModule(SmartModule):
|
class Alarm(SmartModule):
|
||||||
"""Implementation of alarm module."""
|
"""Implementation of alarm module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "alarm"
|
REQUIRED_COMPONENT = "alarm"
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class AutoOffModule(SmartModule):
|
class AutoOff(SmartModule):
|
||||||
"""Implementation of auto off module."""
|
"""Implementation of auto off module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "auto_off"
|
REQUIRED_COMPONENT = "auto_off"
|
@ -2,14 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..smartdevice import SmartDevice
|
|
||||||
|
|
||||||
|
|
||||||
class BatterySensor(SmartModule):
|
class BatterySensor(SmartModule):
|
||||||
"""Implementation of battery module."""
|
"""Implementation of battery module."""
|
||||||
@ -17,23 +12,11 @@ class BatterySensor(SmartModule):
|
|||||||
REQUIRED_COMPONENT = "battery_detect"
|
REQUIRED_COMPONENT = "battery_detect"
|
||||||
QUERY_GETTER_NAME = "get_battery_detect_info"
|
QUERY_GETTER_NAME = "get_battery_detect_info"
|
||||||
|
|
||||||
def __init__(self, device: SmartDevice, module: str):
|
def _initialize_features(self):
|
||||||
super().__init__(device, module)
|
"""Initialize features."""
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
Feature(
|
Feature(
|
||||||
device,
|
self._device,
|
||||||
"battery_level",
|
|
||||||
"Battery level",
|
|
||||||
container=self,
|
|
||||||
attribute_getter="battery",
|
|
||||||
icon="mdi:battery",
|
|
||||||
unit="%",
|
|
||||||
category=Feature.Category.Info,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self._add_feature(
|
|
||||||
Feature(
|
|
||||||
device,
|
|
||||||
"battery_low",
|
"battery_low",
|
||||||
"Battery low",
|
"Battery low",
|
||||||
container=self,
|
container=self,
|
||||||
@ -44,6 +27,22 @@ class BatterySensor(SmartModule):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Some devices, like T110 contact sensor do not report the battery percentage
|
||||||
|
if "battery_percentage" in self._device.sys_info:
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
self._device,
|
||||||
|
"battery_level",
|
||||||
|
"Battery level",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="battery",
|
||||||
|
icon="mdi:battery",
|
||||||
|
unit="%",
|
||||||
|
category=Feature.Category.Info,
|
||||||
|
type=Feature.Type.Sensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def battery(self):
|
def battery(self):
|
||||||
"""Return battery level."""
|
"""Return battery level."""
|
@ -2,16 +2,10 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
BRIGHTNESS_MIN = 0
|
||||||
from ..smartdevice import SmartDevice
|
|
||||||
|
|
||||||
|
|
||||||
BRIGHTNESS_MIN = 1
|
|
||||||
BRIGHTNESS_MAX = 100
|
BRIGHTNESS_MAX = 100
|
||||||
|
|
||||||
|
|
||||||
@ -20,8 +14,11 @@ class Brightness(SmartModule):
|
|||||||
|
|
||||||
REQUIRED_COMPONENT = "brightness"
|
REQUIRED_COMPONENT = "brightness"
|
||||||
|
|
||||||
def __init__(self, device: SmartDevice, module: str):
|
def _initialize_features(self):
|
||||||
super().__init__(device, module)
|
"""Initialize features."""
|
||||||
|
super()._initialize_features()
|
||||||
|
|
||||||
|
device = self._device
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
Feature(
|
Feature(
|
||||||
device,
|
device,
|
||||||
@ -47,8 +44,11 @@ class Brightness(SmartModule):
|
|||||||
"""Return current brightness."""
|
"""Return current brightness."""
|
||||||
return self.data["brightness"]
|
return self.data["brightness"]
|
||||||
|
|
||||||
async def set_brightness(self, brightness: int):
|
async def set_brightness(self, brightness: int, *, transition: int | None = None):
|
||||||
"""Set the brightness."""
|
"""Set the brightness. A brightness value of 0 will turn off the light.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
"""
|
||||||
if not isinstance(brightness, int) or not (
|
if not isinstance(brightness, int) or not (
|
||||||
BRIGHTNESS_MIN <= brightness <= BRIGHTNESS_MAX
|
BRIGHTNESS_MIN <= brightness <= BRIGHTNESS_MAX
|
||||||
):
|
):
|
||||||
@ -57,6 +57,8 @@ class Brightness(SmartModule):
|
|||||||
f"(valid range: {BRIGHTNESS_MIN}-{BRIGHTNESS_MAX}%)"
|
f"(valid range: {BRIGHTNESS_MIN}-{BRIGHTNESS_MAX}%)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if brightness == 0:
|
||||||
|
return await self._device.turn_off()
|
||||||
return await self.call("set_device_info", {"brightness": brightness})
|
return await self.call("set_device_info", {"brightness": brightness})
|
||||||
|
|
||||||
async def _check_supported(self):
|
async def _check_supported(self):
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
class ChildDeviceModule(SmartModule):
|
class ChildDevice(SmartModule):
|
||||||
"""Implementation for child devices."""
|
"""Implementation for child devices."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "child_device"
|
REQUIRED_COMPONENT = "child_device"
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class CloudModule(SmartModule):
|
class Cloud(SmartModule):
|
||||||
"""Implementation of cloud module."""
|
"""Implementation of cloud module."""
|
||||||
|
|
||||||
QUERY_GETTER_NAME = "get_connect_cloud_state"
|
QUERY_GETTER_NAME = "get_connect_cloud_state"
|
@ -4,15 +4,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ...bulb import HSV
|
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
|
from ...interfaces.light import HSV
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class ColorModule(SmartModule):
|
class Color(SmartModule):
|
||||||
"""Implementation of color module."""
|
"""Implementation of color module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "color"
|
REQUIRED_COMPONENT = "color"
|
@ -5,8 +5,8 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from ...bulb import ColorTempRange
|
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
|
from ...interfaces.light import ColorTempRange
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
DEFAULT_TEMP_RANGE = [2500, 6500]
|
DEFAULT_TEMP_RANGE = [2500, 6500]
|
||||||
|
|
||||||
|
|
||||||
class ColorTemperatureModule(SmartModule):
|
class ColorTemperature(SmartModule):
|
||||||
"""Implementation of color temp module."""
|
"""Implementation of color temp module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "color_temperature"
|
REQUIRED_COMPONENT = "color_temperature"
|
42
kasa/smart/modules/contactsensor.py
Normal file
42
kasa/smart/modules/contactsensor.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Implementation of contact sensor module."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from ...feature import Feature
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
|
class ContactSensor(SmartModule):
|
||||||
|
"""Implementation of contact sensor module."""
|
||||||
|
|
||||||
|
REQUIRED_COMPONENT = None # we depend on availability of key
|
||||||
|
REQUIRED_KEY_ON_PARENT = "open"
|
||||||
|
|
||||||
|
def __init__(self, device: SmartDevice, module: str):
|
||||||
|
super().__init__(device, module)
|
||||||
|
self._add_feature(
|
||||||
|
Feature(
|
||||||
|
device,
|
||||||
|
id="is_open",
|
||||||
|
name="Open",
|
||||||
|
container=self,
|
||||||
|
attribute_getter="is_open",
|
||||||
|
icon="mdi:door",
|
||||||
|
category=Feature.Category.Primary,
|
||||||
|
type=Feature.Type.BinarySensor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_open(self):
|
||||||
|
"""Return True if the contact sensor is open."""
|
||||||
|
return self._device.sys_info["open"]
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class EnergyModule(SmartModule):
|
class Energy(SmartModule):
|
||||||
"""Implementation of energy monitoring module."""
|
"""Implementation of energy monitoring module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "energy_monitoring"
|
REQUIRED_COMPONENT = "energy_monitoring"
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class FanModule(SmartModule):
|
class Fan(SmartModule):
|
||||||
"""Implementation of fan_control module."""
|
"""Implementation of fan_control module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "fan_control"
|
REQUIRED_COMPONENT = "fan_control"
|
@ -5,10 +5,8 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
from typing import TYPE_CHECKING, Any, Callable, Coroutine, Optional
|
||||||
|
|
||||||
# When support for cpython older than 3.11 is dropped
|
|
||||||
# async_timeout can be replaced with asyncio.timeout
|
|
||||||
# When support for cpython older than 3.11 is dropped
|
# When support for cpython older than 3.11 is dropped
|
||||||
# async_timeout can be replaced with asyncio.timeout
|
# async_timeout can be replaced with asyncio.timeout
|
||||||
from async_timeout import timeout as asyncio_timeout
|
from async_timeout import timeout as asyncio_timeout
|
||||||
@ -16,9 +14,12 @@ from pydantic.v1 import BaseModel, Field, validator
|
|||||||
|
|
||||||
from ...exceptions import SmartErrorCode
|
from ...exceptions import SmartErrorCode
|
||||||
from ...feature import Feature
|
from ...feature import Feature
|
||||||
from ...firmware import Firmware as FirmwareInterface
|
from ...interfaces import Firmware as FirmwareInterface
|
||||||
from ...firmware import FirmwareUpdate as FirmwareUpdateInterface
|
from ...interfaces.firmware import (
|
||||||
from ...firmware import UpdateResult
|
FirmwareDownloadState as FirmwareDownloadStateInterface,
|
||||||
|
)
|
||||||
|
from ...interfaces.firmware import FirmwareUpdateInfo as FirmwareUpdateInfoInterface
|
||||||
|
from ...interfaces.firmware import UpdateResult
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -28,7 +29,20 @@ if TYPE_CHECKING:
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FirmwareUpdate(BaseModel):
|
class DownloadState(BaseModel):
|
||||||
|
"""Download state."""
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# {'status': 0, 'download_progress': 0, 'reboot_time': 5,
|
||||||
|
# 'upgrade_time': 5, 'auto_upgrade': False}
|
||||||
|
status: int
|
||||||
|
progress: int = Field(alias="download_progress")
|
||||||
|
reboot_time: int
|
||||||
|
upgrade_time: int
|
||||||
|
auto_upgrade: bool
|
||||||
|
|
||||||
|
|
||||||
|
class FirmwareUpdateInfo(BaseModel):
|
||||||
"""Update info status object."""
|
"""Update info status object."""
|
||||||
|
|
||||||
status: int = Field(alias="type")
|
status: int = Field(alias="type")
|
||||||
@ -91,7 +105,7 @@ class Firmware(SmartModule, FirmwareInterface):
|
|||||||
name="Current firmware version",
|
name="Current firmware version",
|
||||||
container=self,
|
container=self,
|
||||||
attribute_getter="current_firmware",
|
attribute_getter="current_firmware",
|
||||||
category=Feature.Category.Info,
|
category=Feature.Category.Debug,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
@ -101,7 +115,7 @@ class Firmware(SmartModule, FirmwareInterface):
|
|||||||
name="Available firmware version",
|
name="Available firmware version",
|
||||||
container=self,
|
container=self,
|
||||||
attribute_getter="latest_firmware",
|
attribute_getter="latest_firmware",
|
||||||
category=Feature.Category.Info,
|
category=Feature.Category.Debug,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -128,9 +142,9 @@ class Firmware(SmartModule, FirmwareInterface):
|
|||||||
fw = self.data.get("get_latest_fw") or self.data
|
fw = self.data.get("get_latest_fw") or self.data
|
||||||
if not self._device.is_cloud_connected or isinstance(fw, SmartErrorCode):
|
if not self._device.is_cloud_connected or isinstance(fw, SmartErrorCode):
|
||||||
# Error in response, probably disconnected from the cloud.
|
# Error in response, probably disconnected from the cloud.
|
||||||
return FirmwareUpdate(type=0, need_to_upgrade=False)
|
return FirmwareUpdateInfo(type=0, need_to_upgrade=False)
|
||||||
|
|
||||||
return FirmwareUpdate.parse_obj(fw)
|
return FirmwareUpdateInfo.parse_obj(fw)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def update_available(self) -> bool | None:
|
def update_available(self) -> bool | None:
|
||||||
@ -139,31 +153,60 @@ class Firmware(SmartModule, FirmwareInterface):
|
|||||||
return None
|
return None
|
||||||
return self.firmware_update_info.update_available
|
return self.firmware_update_info.update_available
|
||||||
|
|
||||||
async def get_update_state(self):
|
async def get_update_state(self) -> DownloadState:
|
||||||
"""Return update state."""
|
"""Return update state."""
|
||||||
return await self.call("get_fw_download_state")
|
resp = await self.call("get_fw_download_state")
|
||||||
|
state = resp["get_fw_download_state"]
|
||||||
|
return DownloadState(**state)
|
||||||
|
|
||||||
async def update(self):
|
async def update(
|
||||||
|
self, progress_cb: Callable[[DownloadState], Coroutine] | None = None
|
||||||
|
):
|
||||||
"""Update the device firmware."""
|
"""Update the device firmware."""
|
||||||
current_fw = self.current_firmware
|
current_fw = self.current_firmware
|
||||||
_LOGGER.debug(
|
_LOGGER.info(
|
||||||
"Going to upgrade from %s to %s",
|
"Going to upgrade from %s to %s",
|
||||||
current_fw,
|
current_fw,
|
||||||
self.firmware_update_info.version,
|
self.firmware_update_info.version,
|
||||||
)
|
)
|
||||||
resp = await self.call("fw_download")
|
await self.call("fw_download")
|
||||||
_LOGGER.debug("Update request response: %s", resp)
|
|
||||||
# TODO: read timeout from get_auto_update_info or from get_fw_download_state?
|
# TODO: read timeout from get_auto_update_info or from get_fw_download_state?
|
||||||
async with asyncio_timeout(60 * 5):
|
async with asyncio_timeout(60 * 5):
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
try:
|
||||||
state = await self.get_update_state()
|
state = await self.get_update_state()
|
||||||
_LOGGER.debug("Update state: %s" % state)
|
except Exception as ex:
|
||||||
# TODO: this could await a given callable for progress
|
_LOGGER.warning(
|
||||||
|
"Got exception, maybe the device is rebooting? %s", ex
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
if self.firmware_update_info.version != current_fw:
|
_LOGGER.debug("Update state: %s" % state)
|
||||||
_LOGGER.info("Updated to %s", self.firmware_update_info.version)
|
if progress_cb is not None:
|
||||||
|
asyncio.create_task(progress_cb(state))
|
||||||
|
|
||||||
|
if state.status == 0:
|
||||||
|
_LOGGER.info(
|
||||||
|
"Update idle, hopefully updated to %s",
|
||||||
|
self.firmware_update_info.version,
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
elif state.status == 2:
|
||||||
|
_LOGGER.info("Downloading firmware, progress: %s", state.progress)
|
||||||
|
elif state.status == 3:
|
||||||
|
upgrade_sleep = state.upgrade_time
|
||||||
|
_LOGGER.info(
|
||||||
|
"Flashing firmware, sleeping for %s before checking status",
|
||||||
|
upgrade_sleep,
|
||||||
|
)
|
||||||
|
await asyncio.sleep(upgrade_sleep)
|
||||||
|
elif state.status < 0:
|
||||||
|
_LOGGER.error("Got error: %s", state.status)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Unhandled state code: %s", state)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def auto_update_enabled(self):
|
def auto_update_enabled(self):
|
||||||
@ -178,16 +221,21 @@ class Firmware(SmartModule, FirmwareInterface):
|
|||||||
data = {**self.data["get_auto_update_info"], "enable": enabled}
|
data = {**self.data["get_auto_update_info"], "enable": enabled}
|
||||||
await self.call("set_auto_update_info", data)
|
await self.call("set_auto_update_info", data)
|
||||||
|
|
||||||
async def update_firmware(self, *, progress_cb) -> UpdateResult:
|
async def update_firmware(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
progress_cb: Callable[[FirmwareDownloadStateInterface], Coroutine]
|
||||||
|
| None = None,
|
||||||
|
) -> UpdateResult:
|
||||||
"""Update the firmware."""
|
"""Update the firmware."""
|
||||||
# TODO: implement, this is part of the common firmware API
|
# TODO: implement, this is part of the common firmware API
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def check_for_updates(self) -> FirmwareUpdateInterface:
|
async def check_for_updates(self) -> FirmwareUpdateInfoInterface:
|
||||||
"""Return firmware update information."""
|
"""Return firmware update information."""
|
||||||
# TODO: naming of the common firmware API methods
|
# TODO: naming of the common firmware API methods
|
||||||
info = self.firmware_update_info
|
info = self.firmware_update_info
|
||||||
return FirmwareUpdateInterface(
|
return FirmwareUpdateInfoInterface(
|
||||||
current_version=self.current_firmware,
|
current_version=self.current_firmware,
|
||||||
update_available=info.update_available,
|
update_available=info.update_available,
|
||||||
available_version=info.version,
|
available_version=info.version,
|
||||||
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class FrostProtectionModule(SmartModule):
|
class FrostProtection(SmartModule):
|
||||||
"""Implementation for frost protection module.
|
"""Implementation for frost protection module.
|
||||||
|
|
||||||
This basically turns the thermostat on and off.
|
This basically turns the thermostat on and off.
|
||||||
|
@ -2,37 +2,16 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from ...interfaces.led import Led as LedInterface
|
||||||
|
|
||||||
from ...feature import Feature
|
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from ..smartdevice import SmartDevice
|
|
||||||
|
|
||||||
|
class Led(SmartModule, LedInterface):
|
||||||
class LedModule(SmartModule):
|
|
||||||
"""Implementation of led controls."""
|
"""Implementation of led controls."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "led"
|
REQUIRED_COMPONENT = "led"
|
||||||
QUERY_GETTER_NAME = "get_led_info"
|
QUERY_GETTER_NAME = "get_led_info"
|
||||||
|
|
||||||
def __init__(self, device: SmartDevice, module: str):
|
|
||||||
super().__init__(device, module)
|
|
||||||
self._add_feature(
|
|
||||||
Feature(
|
|
||||||
device=device,
|
|
||||||
container=self,
|
|
||||||
id="led",
|
|
||||||
name="LED",
|
|
||||||
icon="mdi:led-{state}",
|
|
||||||
attribute_getter="led",
|
|
||||||
attribute_setter="set_led",
|
|
||||||
type=Feature.Type.Switch,
|
|
||||||
category=Feature.Category.Config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def query(self) -> dict:
|
def query(self) -> dict:
|
||||||
"""Query to execute during the update cycle."""
|
"""Query to execute during the update cycle."""
|
||||||
return {self.QUERY_GETTER_NAME: {"led_rule": None}}
|
return {self.QUERY_GETTER_NAME: {"led_rule": None}}
|
||||||
@ -56,7 +35,7 @@ class LedModule(SmartModule):
|
|||||||
This should probably be a select with always/never/nightmode.
|
This should probably be a select with always/never/nightmode.
|
||||||
"""
|
"""
|
||||||
rule = "always" if enable else "never"
|
rule = "always" if enable else "never"
|
||||||
return await self.call("set_led_info", self.data | {"led_rule": rule})
|
return await self.call("set_led_info", dict(self.data, **{"led_rule": rule}))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def night_mode_settings(self):
|
def night_mode_settings(self):
|
126
kasa/smart/modules/light.py
Normal file
126
kasa/smart/modules/light.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""Module for led controls."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from ...exceptions import KasaException
|
||||||
|
from ...interfaces.light import HSV, ColorTempRange
|
||||||
|
from ...interfaces.light import Light as LightInterface
|
||||||
|
from ...module import Module
|
||||||
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
|
|
||||||
|
class Light(SmartModule, LightInterface):
|
||||||
|
"""Implementation of a light."""
|
||||||
|
|
||||||
|
def query(self) -> dict:
|
||||||
|
"""Query to execute during the update cycle."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_color(self) -> bool:
|
||||||
|
"""Whether the bulb supports color changes."""
|
||||||
|
return Module.Color in self._device.modules
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_dimmable(self) -> bool:
|
||||||
|
"""Whether the bulb supports brightness changes."""
|
||||||
|
return Module.Brightness in self._device.modules
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_variable_color_temp(self) -> bool:
|
||||||
|
"""Whether the bulb supports color temperature changes."""
|
||||||
|
return Module.ColorTemperature in self._device.modules
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid_temperature_range(self) -> ColorTempRange:
|
||||||
|
"""Return the device-specific white temperature range (in Kelvin).
|
||||||
|
|
||||||
|
:return: White temperature range in Kelvin (minimum, maximum)
|
||||||
|
"""
|
||||||
|
if not self.is_variable_color_temp:
|
||||||
|
raise KasaException("Color temperature not supported")
|
||||||
|
|
||||||
|
return self._device.modules[Module.ColorTemperature].valid_temperature_range
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hsv(self) -> HSV:
|
||||||
|
"""Return the current HSV state of the bulb.
|
||||||
|
|
||||||
|
:return: hue, saturation and value (degrees, %, %)
|
||||||
|
"""
|
||||||
|
if not self.is_color:
|
||||||
|
raise KasaException("Bulb does not support color.")
|
||||||
|
|
||||||
|
return self._device.modules[Module.Color].hsv
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_temp(self) -> int:
|
||||||
|
"""Whether the bulb supports color temperature changes."""
|
||||||
|
if not self.is_variable_color_temp:
|
||||||
|
raise KasaException("Bulb does not support colortemp.")
|
||||||
|
|
||||||
|
return self._device.modules[Module.ColorTemperature].color_temp
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self) -> int:
|
||||||
|
"""Return the current brightness in percentage."""
|
||||||
|
if not self.is_dimmable: # pragma: no cover
|
||||||
|
raise KasaException("Bulb is not dimmable.")
|
||||||
|
|
||||||
|
return self._device.modules[Module.Brightness].brightness
|
||||||
|
|
||||||
|
async def set_hsv(
|
||||||
|
self,
|
||||||
|
hue: int,
|
||||||
|
saturation: int,
|
||||||
|
value: int | None = None,
|
||||||
|
*,
|
||||||
|
transition: int | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Set new HSV.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int hue: hue in degrees
|
||||||
|
:param int saturation: saturation in percentage [0,100]
|
||||||
|
:param int value: value between 1 and 100
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
if not self.is_color:
|
||||||
|
raise KasaException("Bulb does not support color.")
|
||||||
|
|
||||||
|
return await self._device.modules[Module.Color].set_hsv(hue, saturation, value)
|
||||||
|
|
||||||
|
async def set_color_temp(
|
||||||
|
self, temp: int, *, brightness=None, transition: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""Set the color temperature of the device in kelvin.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int temp: The new color temperature, in Kelvin
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
if not self.is_variable_color_temp:
|
||||||
|
raise KasaException("Bulb does not support colortemp.")
|
||||||
|
return await self._device.modules[Module.ColorTemperature].set_color_temp(temp)
|
||||||
|
|
||||||
|
async def set_brightness(
|
||||||
|
self, brightness: int, *, transition: int | None = None
|
||||||
|
) -> dict:
|
||||||
|
"""Set the brightness in percentage.
|
||||||
|
|
||||||
|
Note, transition is not supported and will be ignored.
|
||||||
|
|
||||||
|
:param int brightness: brightness in percent
|
||||||
|
:param int transition: transition in milliseconds.
|
||||||
|
"""
|
||||||
|
if not self.is_dimmable: # pragma: no cover
|
||||||
|
raise KasaException("Bulb is not dimmable.")
|
||||||
|
|
||||||
|
return await self._device.modules[Module.Brightness].set_brightness(brightness)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_effects(self) -> bool:
|
||||||
|
"""Return True if the device supports effects."""
|
||||||
|
return Module.LightEffect in self._device.modules
|
@ -6,14 +6,14 @@ import base64
|
|||||||
import copy
|
import copy
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from ...feature import Feature
|
from ...interfaces.lighteffect import LightEffect as LightEffectInterface
|
||||||
from ..smartmodule import SmartModule
|
from ..smartmodule import SmartModule
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class LightEffectModule(SmartModule):
|
class LightEffect(SmartModule, LightEffectInterface):
|
||||||
"""Implementation of dynamic light effects."""
|
"""Implementation of dynamic light effects."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "light_effect"
|
REQUIRED_COMPONENT = "light_effect"
|
||||||
@ -22,29 +22,11 @@ class LightEffectModule(SmartModule):
|
|||||||
"L1": "Party",
|
"L1": "Party",
|
||||||
"L2": "Relax",
|
"L2": "Relax",
|
||||||
}
|
}
|
||||||
LIGHT_EFFECTS_OFF = "Off"
|
|
||||||
|
|
||||||
def __init__(self, device: SmartDevice, module: str):
|
def __init__(self, device: SmartDevice, module: str):
|
||||||
super().__init__(device, module)
|
super().__init__(device, module)
|
||||||
self._scenes_names_to_id: dict[str, str] = {}
|
self._scenes_names_to_id: dict[str, str] = {}
|
||||||
|
|
||||||
def _initialize_features(self):
|
|
||||||
"""Initialize features."""
|
|
||||||
device = self._device
|
|
||||||
self._add_feature(
|
|
||||||
Feature(
|
|
||||||
device,
|
|
||||||
id="light_effect",
|
|
||||||
name="Light effect",
|
|
||||||
container=self,
|
|
||||||
attribute_getter="effect",
|
|
||||||
attribute_setter="set_effect",
|
|
||||||
category=Feature.Category.Config,
|
|
||||||
type=Feature.Type.Choice,
|
|
||||||
choices_getter="effect_list",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _initialize_effects(self) -> dict[str, dict[str, Any]]:
|
def _initialize_effects(self) -> dict[str, dict[str, Any]]:
|
||||||
"""Return built-in effects."""
|
"""Return built-in effects."""
|
||||||
# Copy the effects so scene name updates do not update the underlying dict.
|
# Copy the effects so scene name updates do not update the underlying dict.
|
||||||
@ -64,7 +46,7 @@ class LightEffectModule(SmartModule):
|
|||||||
return effects
|
return effects
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effect_list(self) -> list[str] | None:
|
def effect_list(self) -> list[str]:
|
||||||
"""Return built-in effects list.
|
"""Return built-in effects list.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@ -90,6 +72,9 @@ class LightEffectModule(SmartModule):
|
|||||||
async def set_effect(
|
async def set_effect(
|
||||||
self,
|
self,
|
||||||
effect: str,
|
effect: str,
|
||||||
|
*,
|
||||||
|
brightness: int | None = None,
|
||||||
|
transition: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set an effect for the device.
|
"""Set an effect for the device.
|
||||||
|
|
||||||
@ -108,6 +93,24 @@ class LightEffectModule(SmartModule):
|
|||||||
params["id"] = effect_id
|
params["id"] = effect_id
|
||||||
return await self.call("set_dynamic_light_effect_rule_enable", params)
|
return await self.call("set_dynamic_light_effect_rule_enable", params)
|
||||||
|
|
||||||
|
async def set_custom_effect(
|
||||||
|
self,
|
||||||
|
effect_dict: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Set a custom effect on the device.
|
||||||
|
|
||||||
|
:param str effect_dict: The custom effect dict to set
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Device does not support setting custom effects. "
|
||||||
|
"Use has_custom_effects to check for support."
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_custom_effects(self) -> bool:
|
||||||
|
"""Return True if the device supports setting custom effects."""
|
||||||
|
return False
|
||||||
|
|
||||||
def query(self) -> dict:
|
def query(self) -> dict:
|
||||||
"""Query to execute during the update cycle."""
|
"""Query to execute during the update cycle."""
|
||||||
return {self.QUERY_GETTER_NAME: {"start_index": 0}}
|
return {self.QUERY_GETTER_NAME: {"start_index": 0}}
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class LightTransitionModule(SmartModule):
|
class LightTransition(SmartModule):
|
||||||
"""Implementation of gradual on/off."""
|
"""Implementation of gradual on/off."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "on_off_gradually"
|
REQUIRED_COMPONENT = "on_off_gradually"
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class ReportModule(SmartModule):
|
class ReportMode(SmartModule):
|
||||||
"""Implementation of report module."""
|
"""Implementation of report module."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "report_mode"
|
REQUIRED_COMPONENT = "report_mode"
|
@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
|||||||
from ..smartdevice import SmartDevice
|
from ..smartdevice import SmartDevice
|
||||||
|
|
||||||
|
|
||||||
class TimeModule(SmartModule):
|
class Time(SmartModule):
|
||||||
"""Implementation of device_local_time."""
|
"""Implementation of device_local_time."""
|
||||||
|
|
||||||
REQUIRED_COMPONENT = "time"
|
REQUIRED_COMPONENT = "time"
|
@ -49,6 +49,7 @@ class SmartChildDevice(SmartDevice):
|
|||||||
"""Return child device type."""
|
"""Return child device type."""
|
||||||
child_device_map = {
|
child_device_map = {
|
||||||
"plug.powerstrip.sub-plug": DeviceType.Plug,
|
"plug.powerstrip.sub-plug": DeviceType.Plug,
|
||||||
|
"subg.trigger.contact-sensor": DeviceType.Sensor,
|
||||||
"subg.trigger.temp-hmdt-sensor": DeviceType.Sensor,
|
"subg.trigger.temp-hmdt-sensor": DeviceType.Sensor,
|
||||||
"subg.trigger.water-leak-sensor": DeviceType.Sensor,
|
"subg.trigger.water-leak-sensor": DeviceType.Sensor,
|
||||||
"kasa.switch.outlet.sub-fan": DeviceType.Fan,
|
"kasa.switch.outlet.sub-fan": DeviceType.Fan,
|
||||||
|
@ -5,32 +5,24 @@ from __future__ import annotations
|
|||||||
import base64
|
import base64
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Mapping, Sequence, cast, overload
|
from typing import TYPE_CHECKING, Any, Mapping, Sequence, cast
|
||||||
|
|
||||||
from ..aestransport import AesTransport
|
from ..aestransport import AesTransport
|
||||||
from ..bulb import HSV, Bulb, BulbPreset, ColorTempRange
|
|
||||||
from ..device import Device, WifiNetwork
|
from ..device import Device, WifiNetwork
|
||||||
from ..device_type import DeviceType
|
from ..device_type import DeviceType
|
||||||
from ..deviceconfig import DeviceConfig
|
from ..deviceconfig import DeviceConfig
|
||||||
from ..emeterstatus import EmeterStatus
|
from ..emeterstatus import EmeterStatus
|
||||||
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
|
from ..exceptions import AuthenticationError, DeviceError, KasaException, SmartErrorCode
|
||||||
from ..fan import Fan
|
|
||||||
from ..feature import Feature
|
from ..feature import Feature
|
||||||
from ..firmware import Firmware
|
from ..module import Module
|
||||||
from ..module import ModuleT
|
from ..modulemapping import ModuleMapping, ModuleName
|
||||||
from ..smartprotocol import SmartProtocol
|
from ..smartprotocol import SmartProtocol
|
||||||
from .modules import (
|
from .modules import (
|
||||||
Brightness,
|
Cloud,
|
||||||
CloudModule,
|
|
||||||
ColorModule,
|
|
||||||
ColorTemperatureModule,
|
|
||||||
DeviceModule,
|
DeviceModule,
|
||||||
EnergyModule,
|
Firmware,
|
||||||
FanModule,
|
Light,
|
||||||
TimeModule,
|
Time,
|
||||||
)
|
|
||||||
from .modules import (
|
|
||||||
Firmware as FirmwareModule,
|
|
||||||
)
|
)
|
||||||
from .smartmodule import SmartModule
|
from .smartmodule import SmartModule
|
||||||
|
|
||||||
@ -41,12 +33,12 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
# the child but only work on the parent. See longer note below in _initialize_modules.
|
# the child but only work on the parent. See longer note below in _initialize_modules.
|
||||||
# This list should be updated when creating new modules that could have the
|
# This list should be updated when creating new modules that could have the
|
||||||
# same issue, homekit perhaps?
|
# same issue, homekit perhaps?
|
||||||
WALL_SWITCH_PARENT_ONLY_MODULES = [DeviceModule, TimeModule, Firmware, CloudModule]
|
WALL_SWITCH_PARENT_ONLY_MODULES = [DeviceModule, Time, Firmware, Cloud]
|
||||||
|
|
||||||
|
|
||||||
# Device must go last as the other interfaces also inherit Device
|
# Device must go last as the other interfaces also inherit Device
|
||||||
# and python needs a consistent method resolution order.
|
# and python needs a consistent method resolution order.
|
||||||
class SmartDevice(Bulb, Fan, Device):
|
class SmartDevice(Device):
|
||||||
"""Base class to represent a SMART protocol based device."""
|
"""Base class to represent a SMART protocol based device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@ -64,7 +56,7 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
self._components_raw: dict[str, Any] | None = None
|
self._components_raw: dict[str, Any] | None = None
|
||||||
self._components: dict[str, int] = {}
|
self._components: dict[str, int] = {}
|
||||||
self._state_information: dict[str, Any] = {}
|
self._state_information: dict[str, Any] = {}
|
||||||
self._modules: dict[str, SmartModule] = {}
|
self._modules: dict[str | ModuleName[Module], SmartModule] = {}
|
||||||
self._exposes_child_modules = False
|
self._exposes_child_modules = False
|
||||||
self._parent: SmartDevice | None = None
|
self._parent: SmartDevice | None = None
|
||||||
self._children: Mapping[str, SmartDevice] = {}
|
self._children: Mapping[str, SmartDevice] = {}
|
||||||
@ -105,8 +97,20 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
return list(self._children.values())
|
return list(self._children.values())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def modules(self) -> dict[str, SmartModule]:
|
def modules(self) -> ModuleMapping[SmartModule]:
|
||||||
"""Return the device modules."""
|
"""Return the device modules."""
|
||||||
|
if self._exposes_child_modules:
|
||||||
|
modules = {k: v for k, v in self._modules.items()}
|
||||||
|
for child in self._children.values():
|
||||||
|
for k, v in child._modules.items():
|
||||||
|
if k not in modules:
|
||||||
|
modules[k] = v
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
return cast(ModuleMapping[SmartModule], modules)
|
||||||
|
return modules
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # Needed for python 3.8
|
||||||
|
return cast(ModuleMapping[SmartModule], self._modules)
|
||||||
return self._modules
|
return self._modules
|
||||||
|
|
||||||
def _try_get_response(self, responses: dict, request: str, default=None) -> dict:
|
def _try_get_response(self, responses: dict, request: str, default=None) -> dict:
|
||||||
@ -213,7 +217,10 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
skip_parent_only_modules and mod in WALL_SWITCH_PARENT_ONLY_MODULES
|
skip_parent_only_modules and mod in WALL_SWITCH_PARENT_ONLY_MODULES
|
||||||
) or mod.__name__ in child_modules_to_skip:
|
) or mod.__name__ in child_modules_to_skip:
|
||||||
continue
|
continue
|
||||||
if mod.REQUIRED_COMPONENT in self._components:
|
if (
|
||||||
|
mod.REQUIRED_COMPONENT in self._components
|
||||||
|
or self.sys_info.get(mod.REQUIRED_KEY_ON_PARENT) is not None
|
||||||
|
):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Found required %s, adding %s to modules.",
|
"Found required %s, adding %s to modules.",
|
||||||
mod.REQUIRED_COMPONENT,
|
mod.REQUIRED_COMPONENT,
|
||||||
@ -223,6 +230,13 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
if await module._check_supported():
|
if await module._check_supported():
|
||||||
self._modules[module.name] = module
|
self._modules[module.name] = module
|
||||||
|
|
||||||
|
if (
|
||||||
|
Module.Brightness in self._modules
|
||||||
|
or Module.Color in self._modules
|
||||||
|
or Module.ColorTemperature in self._modules
|
||||||
|
):
|
||||||
|
self._modules[Light.__name__] = Light(self, "light")
|
||||||
|
|
||||||
async def _initialize_features(self):
|
async def _initialize_features(self):
|
||||||
"""Initialize device features."""
|
"""Initialize device features."""
|
||||||
self._add_feature(
|
self._add_feature(
|
||||||
@ -310,41 +324,20 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
for module in self._modules.values():
|
for module in self.modules.values():
|
||||||
|
# Check if module features have already been initialized.
|
||||||
|
# i.e. when _exposes_child_modules is true
|
||||||
|
if not module._module_features:
|
||||||
module._initialize_features()
|
module._initialize_features()
|
||||||
for feat in module._module_features.values():
|
for feat in module._module_features.values():
|
||||||
self._add_feature(feat)
|
self._add_feature(feat)
|
||||||
|
|
||||||
@overload
|
|
||||||
def get_module(self, module_type: type[ModuleT]) -> ModuleT | None: ...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def get_module(self, module_type: str) -> SmartModule | None: ...
|
|
||||||
|
|
||||||
def get_module(
|
|
||||||
self, module_type: type[ModuleT] | str
|
|
||||||
) -> ModuleT | SmartModule | None:
|
|
||||||
"""Return the module from the device modules or None if not present."""
|
|
||||||
if isinstance(module_type, str):
|
|
||||||
module_name = module_type
|
|
||||||
elif issubclass(module_type, SmartModule):
|
|
||||||
module_name = module_type.__name__
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
if module_name in self.modules:
|
|
||||||
return self.modules[module_name]
|
|
||||||
elif self._exposes_child_modules:
|
|
||||||
for child in self._children.values():
|
|
||||||
if module_name in child.modules:
|
|
||||||
return child.modules[module_name]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_cloud_connected(self):
|
def is_cloud_connected(self) -> bool:
|
||||||
"""Returns if the device is connected to the cloud."""
|
"""Returns if the device is connected to the cloud."""
|
||||||
if "CloudModule" not in self.modules:
|
if Module.Cloud not in self.modules:
|
||||||
return False
|
return False
|
||||||
return self.modules["CloudModule"].is_connected
|
return self.modules[Module.Cloud].is_connected
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sys_info(self) -> dict[str, Any]:
|
def sys_info(self) -> dict[str, Any]:
|
||||||
@ -368,10 +361,10 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
def time(self) -> datetime:
|
def time(self) -> datetime:
|
||||||
"""Return the time."""
|
"""Return the time."""
|
||||||
# TODO: Default to parent's time module for child devices
|
# TODO: Default to parent's time module for child devices
|
||||||
if self._parent and "TimeModule" in self.modules:
|
if self._parent and Module.Time in self.modules:
|
||||||
_timemod = cast(TimeModule, self._parent.modules["TimeModule"]) # noqa: F405
|
_timemod = self._parent.modules[Module.Time]
|
||||||
else:
|
else:
|
||||||
_timemod = cast(TimeModule, self.modules["TimeModule"]) # noqa: F405
|
_timemod = self.modules[Module.Time]
|
||||||
|
|
||||||
return _timemod.time
|
return _timemod.time
|
||||||
|
|
||||||
@ -448,12 +441,7 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
@property
|
@property
|
||||||
def has_emeter(self) -> bool:
|
def has_emeter(self) -> bool:
|
||||||
"""Return if the device has emeter."""
|
"""Return if the device has emeter."""
|
||||||
return "EnergyModule" in self.modules
|
return Module.Energy in self.modules
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dimmer(self) -> bool:
|
|
||||||
"""Whether the device acts as a dimmer."""
|
|
||||||
return self.is_dimmable
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -490,19 +478,19 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
@property
|
@property
|
||||||
def emeter_realtime(self) -> EmeterStatus:
|
def emeter_realtime(self) -> EmeterStatus:
|
||||||
"""Get the emeter status."""
|
"""Get the emeter status."""
|
||||||
energy = cast(EnergyModule, self.modules["EnergyModule"])
|
energy = self.modules[Module.Energy]
|
||||||
return energy.emeter_realtime
|
return energy.emeter_realtime
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def emeter_this_month(self) -> float | None:
|
def emeter_this_month(self) -> float | None:
|
||||||
"""Get the emeter value for this month."""
|
"""Get the emeter value for this month."""
|
||||||
energy = cast(EnergyModule, self.modules["EnergyModule"])
|
energy = self.modules[Module.Energy]
|
||||||
return energy.emeter_this_month
|
return energy.emeter_this_month
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def emeter_today(self) -> float | None:
|
def emeter_today(self) -> float | None:
|
||||||
"""Get the emeter value for today."""
|
"""Get the emeter value for today."""
|
||||||
energy = cast(EnergyModule, self.modules["EnergyModule"])
|
energy = self.modules[Module.Energy]
|
||||||
return energy.emeter_today
|
return energy.emeter_today
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -514,8 +502,7 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
on_time = cast(float, on_time)
|
on_time = cast(float, on_time)
|
||||||
if (timemod := self.modules.get("TimeModule")) is not None:
|
if (timemod := self.modules.get(Module.Time)) is not None:
|
||||||
timemod = cast(TimeModule, timemod) # noqa: F405
|
|
||||||
return timemod.time - timedelta(seconds=on_time)
|
return timemod.time - timedelta(seconds=on_time)
|
||||||
else: # We have no device time, use current local time.
|
else: # We have no device time, use current local time.
|
||||||
return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time)
|
return datetime.now().replace(microsecond=0) - timedelta(seconds=on_time)
|
||||||
@ -628,12 +615,6 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
|
|
||||||
return self._device_type
|
return self._device_type
|
||||||
|
|
||||||
@property
|
|
||||||
def firmware(self) -> Firmware:
|
|
||||||
"""Return firmware module."""
|
|
||||||
# TODO: open question: does it make sense to expose common modules?
|
|
||||||
return cast(Firmware, self.get_module(FirmwareModule))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_device_type_from_components(
|
def _get_device_type_from_components(
|
||||||
components: list[str], device_type: str
|
components: list[str], device_type: str
|
||||||
@ -661,149 +642,3 @@ class SmartDevice(Bulb, Fan, Device):
|
|||||||
return DeviceType.Thermostat
|
return DeviceType.Thermostat
|
||||||
_LOGGER.warning("Unknown device type, falling back to plug")
|
_LOGGER.warning("Unknown device type, falling back to plug")
|
||||||
return DeviceType.Plug
|
return DeviceType.Plug
|
||||||
|
|
||||||
# Fan interface methods
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_fan(self) -> bool:
|
|
||||||
"""Return True if the device is a fan."""
|
|
||||||
return "FanModule" in self.modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fan_speed_level(self) -> int:
|
|
||||||
"""Return fan speed level."""
|
|
||||||
if not self.is_fan:
|
|
||||||
raise KasaException("Device is not a Fan")
|
|
||||||
return cast(FanModule, self.modules["FanModule"]).fan_speed_level
|
|
||||||
|
|
||||||
async def set_fan_speed_level(self, level: int):
|
|
||||||
"""Set fan speed level."""
|
|
||||||
if not self.is_fan:
|
|
||||||
raise KasaException("Device is not a Fan")
|
|
||||||
await cast(FanModule, self.modules["FanModule"]).set_fan_speed_level(level)
|
|
||||||
|
|
||||||
# Bulb interface methods
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_color(self) -> bool:
|
|
||||||
"""Whether the bulb supports color changes."""
|
|
||||||
return "ColorModule" in self.modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dimmable(self) -> bool:
|
|
||||||
"""Whether the bulb supports brightness changes."""
|
|
||||||
return "Brightness" in self.modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_variable_color_temp(self) -> bool:
|
|
||||||
"""Whether the bulb supports color temperature changes."""
|
|
||||||
return "ColorTemperatureModule" in self.modules
|
|
||||||
|
|
||||||
@property
|
|
||||||
def valid_temperature_range(self) -> ColorTempRange:
|
|
||||||
"""Return the device-specific white temperature range (in Kelvin).
|
|
||||||
|
|
||||||
:return: White temperature range in Kelvin (minimum, maximum)
|
|
||||||
"""
|
|
||||||
if not self.is_variable_color_temp:
|
|
||||||
raise KasaException("Color temperature not supported")
|
|
||||||
|
|
||||||
return cast(
|
|
||||||
ColorTemperatureModule, self.modules["ColorTemperatureModule"]
|
|
||||||
).valid_temperature_range
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hsv(self) -> HSV:
|
|
||||||
"""Return the current HSV state of the bulb.
|
|
||||||
|
|
||||||
:return: hue, saturation and value (degrees, %, %)
|
|
||||||
"""
|
|
||||||
if not self.is_color:
|
|
||||||
raise KasaException("Bulb does not support color.")
|
|
||||||
|
|
||||||
return cast(ColorModule, self.modules["ColorModule"]).hsv
|
|
||||||
|
|
||||||
@property
|
|
||||||
def color_temp(self) -> int:
|
|
||||||
"""Whether the bulb supports color temperature changes."""
|
|
||||||
if not self.is_variable_color_temp:
|
|
||||||
raise KasaException("Bulb does not support colortemp.")
|
|
||||||
|
|
||||||
return cast(
|
|
||||||
ColorTemperatureModule, self.modules["ColorTemperatureModule"]
|
|
||||||
).color_temp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def brightness(self) -> int:
|
|
||||||
"""Return the current brightness in percentage."""
|
|
||||||
if not self.is_dimmable: # pragma: no cover
|
|
||||||
raise KasaException("Bulb is not dimmable.")
|
|
||||||
|
|
||||||
return cast(Brightness, self.modules["Brightness"]).brightness
|
|
||||||
|
|
||||||
async def set_hsv(
|
|
||||||
self,
|
|
||||||
hue: int,
|
|
||||||
saturation: int,
|
|
||||||
value: int | None = None,
|
|
||||||
*,
|
|
||||||
transition: int | None = None,
|
|
||||||
) -> dict:
|
|
||||||
"""Set new HSV.
|
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
|
||||||
|
|
||||||
:param int hue: hue in degrees
|
|
||||||
:param int saturation: saturation in percentage [0,100]
|
|
||||||
:param int value: value between 1 and 100
|
|
||||||
:param int transition: transition in milliseconds.
|
|
||||||
"""
|
|
||||||
if not self.is_color:
|
|
||||||
raise KasaException("Bulb does not support color.")
|
|
||||||
|
|
||||||
return await cast(ColorModule, self.modules["ColorModule"]).set_hsv(
|
|
||||||
hue, saturation, value
|
|
||||||
)
|
|
||||||
|
|
||||||
async def set_color_temp(
|
|
||||||
self, temp: int, *, brightness=None, transition: int | None = None
|
|
||||||
) -> dict:
|
|
||||||
"""Set the color temperature of the device in kelvin.
|
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
|
||||||
|
|
||||||
:param int temp: The new color temperature, in Kelvin
|
|
||||||
:param int transition: transition in milliseconds.
|
|
||||||
"""
|
|
||||||
if not self.is_variable_color_temp:
|
|
||||||
raise KasaException("Bulb does not support colortemp.")
|
|
||||||
return await cast(
|
|
||||||
ColorTemperatureModule, self.modules["ColorTemperatureModule"]
|
|
||||||
).set_color_temp(temp)
|
|
||||||
|
|
||||||
async def set_brightness(
|
|
||||||
self, brightness: int, *, transition: int | None = None
|
|
||||||
) -> dict:
|
|
||||||
"""Set the brightness in percentage.
|
|
||||||
|
|
||||||
Note, transition is not supported and will be ignored.
|
|
||||||
|
|
||||||
:param int brightness: brightness in percent
|
|
||||||
:param int transition: transition in milliseconds.
|
|
||||||
"""
|
|
||||||
if not self.is_dimmable: # pragma: no cover
|
|
||||||
raise KasaException("Bulb is not dimmable.")
|
|
||||||
|
|
||||||
return await cast(Brightness, self.modules["Brightness"]).set_brightness(
|
|
||||||
brightness
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def presets(self) -> list[BulbPreset]:
|
|
||||||
"""Return a list of available bulb setting presets."""
|
|
||||||
return []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_effects(self) -> bool:
|
|
||||||
"""Return True if the device supports effects."""
|
|
||||||
return "LightEffectModule" in self.modules
|
|
||||||
|
@ -18,8 +18,13 @@ class SmartModule(Module):
|
|||||||
"""Base class for SMART modules."""
|
"""Base class for SMART modules."""
|
||||||
|
|
||||||
NAME: str
|
NAME: str
|
||||||
REQUIRED_COMPONENT: str
|
#: Module is initialized, if the given component is available
|
||||||
|
REQUIRED_COMPONENT: str | None = None
|
||||||
|
#: Module is initialized, if the given key available in the main sysinfo
|
||||||
|
REQUIRED_KEY_ON_PARENT: str | None = None
|
||||||
|
#: Query to execute during the main update cycle
|
||||||
QUERY_GETTER_NAME: str
|
QUERY_GETTER_NAME: str
|
||||||
|
|
||||||
REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
|
REGISTERED_MODULES: dict[str, type[SmartModule]] = {}
|
||||||
|
|
||||||
def __init__(self, device: SmartDevice, module: str):
|
def __init__(self, device: SmartDevice, module: str):
|
||||||
@ -27,8 +32,6 @@ class SmartModule(Module):
|
|||||||
super().__init__(device, module)
|
super().__init__(device, module)
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs):
|
||||||
assert cls.REQUIRED_COMPONENT is not None # noqa: S101
|
|
||||||
|
|
||||||
name = getattr(cls, "NAME", cls.__name__)
|
name = getattr(cls, "NAME", cls.__name__)
|
||||||
_LOGGER.debug("Registering %s" % cls)
|
_LOGGER.debug("Registering %s" % cls)
|
||||||
cls.REGISTERED_MODULES[name] = cls
|
cls.REGISTERED_MODULES[name] = cls
|
||||||
@ -91,8 +94,13 @@ class SmartModule(Module):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_version(self) -> int:
|
def supported_version(self) -> int:
|
||||||
"""Return version supported by the device."""
|
"""Return version supported by the device.
|
||||||
|
|
||||||
|
If the module has no required component, this will return -1.
|
||||||
|
"""
|
||||||
|
if self.REQUIRED_COMPONENT is not None:
|
||||||
return self._device._components[self.REQUIRED_COMPONENT]
|
return self._device._components[self.REQUIRED_COMPONENT]
|
||||||
|
return -1
|
||||||
|
|
||||||
async def _check_supported(self) -> bool:
|
async def _check_supported(self) -> bool:
|
||||||
"""Additional check to see if the module is supported by the device.
|
"""Additional check to see if the module is supported by the device.
|
||||||
|
@ -109,7 +109,7 @@ DIMMERS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HUBS_SMART = {"H100", "KH100"}
|
HUBS_SMART = {"H100", "KH100"}
|
||||||
SENSORS_SMART = {"T310", "T315", "T300"}
|
SENSORS_SMART = {"T310", "T315", "T300", "T110"}
|
||||||
THERMOSTATS_SMART = {"KE100"}
|
THERMOSTATS_SMART = {"KE100"}
|
||||||
|
|
||||||
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
|
WITH_EMETER_IOT = {"HS110", "HS300", "KP115", "KP125", *BULBS_IOT}
|
||||||
@ -203,14 +203,14 @@ wallswitch_iot = parametrize(
|
|||||||
"wall switches iot", model_filter=SWITCHES, protocol_filter={"IOT"}
|
"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_iot = parametrize("dimmers", model_filter=DIMMERS, protocol_filter={"IOT"})
|
||||||
lightstrip = parametrize(
|
lightstrip_iot = parametrize(
|
||||||
"lightstrips", model_filter=LIGHT_STRIPS, protocol_filter={"IOT"}
|
"lightstrips", model_filter=LIGHT_STRIPS, protocol_filter={"IOT"}
|
||||||
)
|
)
|
||||||
|
|
||||||
# bulb types
|
# bulb types
|
||||||
dimmable = parametrize("dimmable", model_filter=DIMMABLE, protocol_filter={"IOT"})
|
dimmable_iot = parametrize("dimmable", model_filter=DIMMABLE, protocol_filter={"IOT"})
|
||||||
non_dimmable = parametrize(
|
non_dimmable_iot = parametrize(
|
||||||
"non-dimmable", model_filter=BULBS - DIMMABLE, protocol_filter={"IOT"}
|
"non-dimmable", model_filter=BULBS - DIMMABLE, protocol_filter={"IOT"}
|
||||||
)
|
)
|
||||||
variable_temp = parametrize(
|
variable_temp = parametrize(
|
||||||
@ -292,12 +292,12 @@ device_iot = parametrize(
|
|||||||
def check_categories():
|
def check_categories():
|
||||||
"""Check that every fixture file is categorized."""
|
"""Check that every fixture file is categorized."""
|
||||||
categorized_fixtures = set(
|
categorized_fixtures = set(
|
||||||
dimmer.args[1]
|
dimmer_iot.args[1]
|
||||||
+ strip.args[1]
|
+ strip.args[1]
|
||||||
+ plug.args[1]
|
+ plug.args[1]
|
||||||
+ bulb.args[1]
|
+ bulb.args[1]
|
||||||
+ wallswitch.args[1]
|
+ wallswitch.args[1]
|
||||||
+ lightstrip.args[1]
|
+ lightstrip_iot.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]
|
||||||
|
@ -189,6 +189,11 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
if "current_rule_id" in info["get_dynamic_light_effect_rules"]:
|
if "current_rule_id" in info["get_dynamic_light_effect_rules"]:
|
||||||
del info["get_dynamic_light_effect_rules"]["current_rule_id"]
|
del info["get_dynamic_light_effect_rules"]["current_rule_id"]
|
||||||
|
|
||||||
|
def _set_led_info(self, info, params):
|
||||||
|
"""Set or remove values as per the device behaviour."""
|
||||||
|
info["get_led_info"]["led_status"] = params["led_rule"] != "never"
|
||||||
|
info["get_led_info"]["led_rule"] = params["led_rule"]
|
||||||
|
|
||||||
def _send_request(self, request_dict: dict):
|
def _send_request(self, request_dict: dict):
|
||||||
method = request_dict["method"]
|
method = request_dict["method"]
|
||||||
params = request_dict["params"]
|
params = request_dict["params"]
|
||||||
@ -218,7 +223,9 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
# SMART fixtures started to be generated
|
# SMART fixtures started to be generated
|
||||||
missing_result := self.FIXTURE_MISSING_MAP.get(method)
|
missing_result := self.FIXTURE_MISSING_MAP.get(method)
|
||||||
) and missing_result[0] in self.components:
|
) and missing_result[0] in self.components:
|
||||||
result = copy.deepcopy(missing_result[1])
|
# Copy to info so it will work with update methods
|
||||||
|
info[method] = copy.deepcopy(missing_result[1])
|
||||||
|
result = copy.deepcopy(info[method])
|
||||||
retval = {"result": result, "error_code": 0}
|
retval = {"result": result, "error_code": 0}
|
||||||
else:
|
else:
|
||||||
# PARAMS error returned for KS240 when get_device_usage called
|
# PARAMS error returned for KS240 when get_device_usage called
|
||||||
@ -234,11 +241,14 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
pytest.fixtures_missing_methods[self.fixture_name] = set()
|
pytest.fixtures_missing_methods[self.fixture_name] = set()
|
||||||
pytest.fixtures_missing_methods[self.fixture_name].add(method)
|
pytest.fixtures_missing_methods[self.fixture_name].add(method)
|
||||||
return retval
|
return retval
|
||||||
elif method == "set_qs_info":
|
elif method in ["set_qs_info", "fw_download"]:
|
||||||
return {"error_code": 0}
|
return {"error_code": 0}
|
||||||
elif method == "set_dynamic_light_effect_rule_enable":
|
elif method == "set_dynamic_light_effect_rule_enable":
|
||||||
self._set_light_effect(info, params)
|
self._set_light_effect(info, params)
|
||||||
return {"error_code": 0}
|
return {"error_code": 0}
|
||||||
|
elif method == "set_led_info":
|
||||||
|
self._set_led_info(info, params)
|
||||||
|
return {"error_code": 0}
|
||||||
elif method[:4] == "set_":
|
elif method[:4] == "set_":
|
||||||
target_method = f"get_{method[4:]}"
|
target_method = f"get_{method[4:]}"
|
||||||
info[target_method].update(params)
|
info[target_method].update(params)
|
||||||
|
547
kasa/tests/fixtures/smart/H100(EU)_1.0_1.5.10.json
vendored
Normal file
547
kasa/tests/fixtures/smart/H100(EU)_1.0_1.5.10.json
vendored
Normal file
@ -0,0 +1,547 @@
|
|||||||
|
{
|
||||||
|
"component_nego": {
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "device",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inherit",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wireless",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "synchronize",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sunrise_sunset",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "led",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloud_connect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child_device",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child_quick_setup",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child_inherit",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "control_child",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alarm",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_load",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_local_time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "alarm_logs",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "localSmart",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "matter",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chime",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"discovery_result": {
|
||||||
|
"device_id": "00000000000000000000000000000000",
|
||||||
|
"device_model": "H100(EU)",
|
||||||
|
"device_type": "SMART.TAPOHUB",
|
||||||
|
"factory_default": false,
|
||||||
|
"ip": "127.0.0.123",
|
||||||
|
"is_support_iot_cloud": true,
|
||||||
|
"mac": "3C-52-A1-00-00-00",
|
||||||
|
"mgt_encrypt_schm": {
|
||||||
|
"encrypt_type": "AES",
|
||||||
|
"http_port": 80,
|
||||||
|
"is_support_https": false,
|
||||||
|
"lv": 2
|
||||||
|
},
|
||||||
|
"obd_src": "tplink",
|
||||||
|
"owner": "00000000000000000000000000000000"
|
||||||
|
},
|
||||||
|
"get_alarm_configure": {
|
||||||
|
"duration": 10,
|
||||||
|
"type": "Alarm 1",
|
||||||
|
"volume": "high"
|
||||||
|
},
|
||||||
|
"get_auto_update_info": {
|
||||||
|
"enable": false,
|
||||||
|
"random_range": 120,
|
||||||
|
"time": 180
|
||||||
|
},
|
||||||
|
"get_child_device_component_list": {
|
||||||
|
"child_component_list": [
|
||||||
|
{
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "device",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_local_time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "schedule",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "countdown",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "synchronize",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sunrise_sunset",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloud_connect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frost_protection",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child_protection",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temperature",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temp_control",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "remove_scale",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "progress_calibration",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "early_start",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temp_record",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "screen_setting",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "night_mode",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "smart_control_schedule",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "battery_detect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temperature_correction",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "window_open_detect",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shutdown_mode",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"device_id": "SCRUBBED_CHILD_DEVICE_ID_1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "device",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trigger_log",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_local_time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "synchronize",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloud_connect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "localSmart",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "battery_detect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temperature",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "humidity",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temp_humidity_record",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "comfort_temperature",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "comfort_humidity",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "report_mode",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"device_id": "SCRUBBED_CHILD_DEVICE_ID_2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_index": 0,
|
||||||
|
"sum": 2
|
||||||
|
},
|
||||||
|
"get_child_device_list": {
|
||||||
|
"child_device_list": [
|
||||||
|
{
|
||||||
|
"at_low_battery": false,
|
||||||
|
"avatar": "",
|
||||||
|
"battery_percentage": 100,
|
||||||
|
"bind_count": 5,
|
||||||
|
"category": "subg.trv",
|
||||||
|
"child_protection": false,
|
||||||
|
"current_temp": 22.9,
|
||||||
|
"device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
|
"frost_protection_on": false,
|
||||||
|
"fw_ver": "2.4.0 Build 230804 Rel.193040",
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"hw_ver": "1.0",
|
||||||
|
"jamming_rssi": -113,
|
||||||
|
"jamming_signal_level": 1,
|
||||||
|
"location": "",
|
||||||
|
"mac": "A842A1000000",
|
||||||
|
"max_control_temp": 30,
|
||||||
|
"min_control_temp": 5,
|
||||||
|
"model": "KE100",
|
||||||
|
"nickname": "I01BU0tFRF9OQU1FIw==",
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"parent_device_id": "0000000000000000000000000000000000000000",
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"rssi": -7,
|
||||||
|
"signal_level": 3,
|
||||||
|
"specs": "EU",
|
||||||
|
"status": "online",
|
||||||
|
"status_follow_edge": false,
|
||||||
|
"target_temp": 23.0,
|
||||||
|
"temp_offset": 0,
|
||||||
|
"temp_unit": "celsius",
|
||||||
|
"trv_states": [
|
||||||
|
"heating"
|
||||||
|
],
|
||||||
|
"type": "SMART.KASAENERGY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"at_low_battery": false,
|
||||||
|
"avatar": "",
|
||||||
|
"battery_percentage": 100,
|
||||||
|
"bind_count": 1,
|
||||||
|
"category": "subg.trigger.temp-hmdt-sensor",
|
||||||
|
"current_humidity": 62,
|
||||||
|
"current_humidity_exception": 2,
|
||||||
|
"current_temp": 24.0,
|
||||||
|
"current_temp_exception": 0,
|
||||||
|
"device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
|
"fw_ver": "1.7.0 Build 230424 Rel.170332",
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"hw_ver": "1.0",
|
||||||
|
"jamming_rssi": -115,
|
||||||
|
"jamming_signal_level": 1,
|
||||||
|
"lastOnboardingTimestamp": 1706990901,
|
||||||
|
"mac": "F0A731000000",
|
||||||
|
"model": "T315",
|
||||||
|
"nickname": "I01BU0tFRF9OQU1FIw==",
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"parent_device_id": "0000000000000000000000000000000000000000",
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"report_interval": 16,
|
||||||
|
"rssi": -38,
|
||||||
|
"signal_level": 3,
|
||||||
|
"specs": "EU",
|
||||||
|
"status": "online",
|
||||||
|
"status_follow_edge": false,
|
||||||
|
"temp_unit": "celsius",
|
||||||
|
"type": "SMART.TAPOSENSOR"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_index": 0,
|
||||||
|
"sum": 2
|
||||||
|
},
|
||||||
|
"get_connect_cloud_state": {
|
||||||
|
"status": 0
|
||||||
|
},
|
||||||
|
"get_device_info": {
|
||||||
|
"avatar": "hub",
|
||||||
|
"device_id": "0000000000000000000000000000000000000000",
|
||||||
|
"fw_id": "00000000000000000000000000000000",
|
||||||
|
"fw_ver": "1.5.10 Build 240207 Rel.175759",
|
||||||
|
"has_set_location_info": true,
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"hw_ver": "1.0",
|
||||||
|
"in_alarm": false,
|
||||||
|
"in_alarm_source": "",
|
||||||
|
"ip": "127.0.0.123",
|
||||||
|
"lang": "de_DE",
|
||||||
|
"latitude": 0,
|
||||||
|
"longitude": 0,
|
||||||
|
"mac": "3C-52-A1-00-00-00",
|
||||||
|
"model": "H100",
|
||||||
|
"nickname": "I01BU0tFRF9OQU1FIw==",
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"overheated": false,
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"rssi": -60,
|
||||||
|
"signal_level": 2,
|
||||||
|
"specs": "EU",
|
||||||
|
"ssid": "I01BU0tFRF9TU0lEIw==",
|
||||||
|
"time_diff": 60,
|
||||||
|
"type": "SMART.TAPOHUB"
|
||||||
|
},
|
||||||
|
"get_device_load_info": {
|
||||||
|
"cur_load_num": 4,
|
||||||
|
"load_level": "light",
|
||||||
|
"max_load_num": 64,
|
||||||
|
"total_memory": 4352,
|
||||||
|
"used_memory": 1451
|
||||||
|
},
|
||||||
|
"get_device_time": {
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"time_diff": 60,
|
||||||
|
"timestamp": 1714669215
|
||||||
|
},
|
||||||
|
"get_device_usage": {},
|
||||||
|
"get_fw_download_state": {
|
||||||
|
"auto_upgrade": false,
|
||||||
|
"download_progress": 0,
|
||||||
|
"reboot_time": 5,
|
||||||
|
"status": 0,
|
||||||
|
"upgrade_time": 5
|
||||||
|
},
|
||||||
|
"get_inherit_info": null,
|
||||||
|
"get_latest_fw": {
|
||||||
|
"fw_size": 0,
|
||||||
|
"fw_ver": "1.5.10 Build 240207 Rel.175759",
|
||||||
|
"hw_id": "",
|
||||||
|
"need_to_upgrade": false,
|
||||||
|
"oem_id": "",
|
||||||
|
"release_date": "",
|
||||||
|
"release_note": "",
|
||||||
|
"type": 0
|
||||||
|
},
|
||||||
|
"get_led_info": {
|
||||||
|
"led_rule": "never",
|
||||||
|
"led_status": false,
|
||||||
|
"night_mode": {
|
||||||
|
"end_time": 358,
|
||||||
|
"night_mode_type": "sunrise_sunset",
|
||||||
|
"start_time": 1259,
|
||||||
|
"sunrise_offset": 0,
|
||||||
|
"sunset_offset": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get_matter_setup_info": {
|
||||||
|
"setup_code": "00000000000",
|
||||||
|
"setup_payload": "00:0000000000000000000"
|
||||||
|
},
|
||||||
|
"get_support_alarm_type_list": {
|
||||||
|
"alarm_type_list": [
|
||||||
|
"Doorbell Ring 1",
|
||||||
|
"Doorbell Ring 2",
|
||||||
|
"Doorbell Ring 3",
|
||||||
|
"Doorbell Ring 4",
|
||||||
|
"Doorbell Ring 5",
|
||||||
|
"Doorbell Ring 6",
|
||||||
|
"Doorbell Ring 7",
|
||||||
|
"Doorbell Ring 8",
|
||||||
|
"Doorbell Ring 9",
|
||||||
|
"Doorbell Ring 10",
|
||||||
|
"Phone Ring",
|
||||||
|
"Alarm 1",
|
||||||
|
"Alarm 2",
|
||||||
|
"Alarm 3",
|
||||||
|
"Alarm 4",
|
||||||
|
"Dripping Tap",
|
||||||
|
"Alarm 5",
|
||||||
|
"Connection 1",
|
||||||
|
"Connection 2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get_support_child_device_category": {
|
||||||
|
"device_category_list": [
|
||||||
|
{
|
||||||
|
"category": "subg.trv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "subg.trigger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "subg.plugswitch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get_wireless_scan_info": {
|
||||||
|
"ap_list": [],
|
||||||
|
"start_index": 0,
|
||||||
|
"sum": 0,
|
||||||
|
"wep_supported": false
|
||||||
|
},
|
||||||
|
"qs_component_nego": {
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sunrise_sunset",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inherit",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "matter",
|
||||||
|
"ver_code": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extra_info": {
|
||||||
|
"device_model": "H100",
|
||||||
|
"device_type": "SMART.TAPOHUB",
|
||||||
|
"is_klap": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
170
kasa/tests/fixtures/smart/child/KE100(EU)_1.0_2.4.0.json
vendored
Normal file
170
kasa/tests/fixtures/smart/child/KE100(EU)_1.0_2.4.0.json
vendored
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
{
|
||||||
|
"component_nego": {
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "device",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_local_time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "schedule",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "countdown",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "synchronize",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sunrise_sunset",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloud_connect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frost_protection",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "child_protection",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temperature",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temp_control",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "remove_scale",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "progress_calibration",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "early_start",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temp_record",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "screen_setting",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "night_mode",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "smart_control_schedule",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "battery_detect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "temperature_correction",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "window_open_detect",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shutdown_mode",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get_connect_cloud_state": {
|
||||||
|
"status": 0
|
||||||
|
},
|
||||||
|
"get_device_info": {
|
||||||
|
"at_low_battery": false,
|
||||||
|
"avatar": "",
|
||||||
|
"battery_percentage": 100,
|
||||||
|
"bind_count": 5,
|
||||||
|
"category": "subg.trv",
|
||||||
|
"child_protection": false,
|
||||||
|
"current_temp": 22.9,
|
||||||
|
"device_id": "SCRUBBED_CHILD_DEVICE_ID_1",
|
||||||
|
"frost_protection_on": false,
|
||||||
|
"fw_ver": "2.4.0 Build 230804 Rel.193040",
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"hw_ver": "1.0",
|
||||||
|
"jamming_rssi": -113,
|
||||||
|
"jamming_signal_level": 1,
|
||||||
|
"lastOnboardingTimestamp": 1713888871,
|
||||||
|
"location": "",
|
||||||
|
"mac": "A842A1000000",
|
||||||
|
"max_control_temp": 30,
|
||||||
|
"min_control_temp": 5,
|
||||||
|
"model": "KE100",
|
||||||
|
"nickname": "I01BU0tFRF9OQU1FIw==",
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"parent_device_id": "0000000000000000000000000000000000000000",
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"rssi": -7,
|
||||||
|
"signal_level": 3,
|
||||||
|
"specs": "EU",
|
||||||
|
"status": "online",
|
||||||
|
"target_temp": 23.0,
|
||||||
|
"temp_offset": 0,
|
||||||
|
"temp_unit": "celsius",
|
||||||
|
"trv_states": [
|
||||||
|
"heating"
|
||||||
|
],
|
||||||
|
"type": "SMART.KASAENERGY"
|
||||||
|
},
|
||||||
|
"get_fw_download_state": {
|
||||||
|
"cloud_cache_seconds": 1,
|
||||||
|
"download_progress": 0,
|
||||||
|
"reboot_time": 5,
|
||||||
|
"status": 0,
|
||||||
|
"upgrade_time": 5
|
||||||
|
},
|
||||||
|
"get_latest_fw": {
|
||||||
|
"fw_ver": "2.8.0 Build 240202 Rel.135229",
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"need_to_upgrade": true,
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"release_date": "2024-02-05",
|
||||||
|
"release_note": "Modifications and Bug Fixes:\n1. Optimized the noise issue in some cases.\n2. Fixed some minor bugs.",
|
||||||
|
"type": 2
|
||||||
|
}
|
||||||
|
}
|
526
kasa/tests/fixtures/smart/child/T110(EU)_1.0_1.8.0.json
vendored
Normal file
526
kasa/tests/fixtures/smart/child/T110(EU)_1.0_1.8.0.json
vendored
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
{
|
||||||
|
"component_nego": {
|
||||||
|
"component_list": [
|
||||||
|
{
|
||||||
|
"id": "device",
|
||||||
|
"ver_code": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "quick_setup",
|
||||||
|
"ver_code": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trigger_log",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "device_local_time",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "account",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "synchronize",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cloud_connect",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "iot_cloud",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firmware",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "localSmart",
|
||||||
|
"ver_code": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "battery_detect",
|
||||||
|
"ver_code": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get_connect_cloud_state": {
|
||||||
|
"status": 0
|
||||||
|
},
|
||||||
|
"get_device_info": {
|
||||||
|
"at_low_battery": false,
|
||||||
|
"avatar": "sensor_t110",
|
||||||
|
"bind_count": 1,
|
||||||
|
"category": "subg.trigger.contact-sensor",
|
||||||
|
"device_id": "SCRUBBED_CHILD_DEVICE_ID_2",
|
||||||
|
"fw_ver": "1.8.0 Build 220728 Rel.160024",
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"hw_ver": "1.0",
|
||||||
|
"jamming_rssi": -113,
|
||||||
|
"jamming_signal_level": 1,
|
||||||
|
"lastOnboardingTimestamp": 1714661626,
|
||||||
|
"mac": "E4FAC4000000",
|
||||||
|
"model": "T110",
|
||||||
|
"nickname": "I01BU0tFRF9OQU1FIw==",
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"open": false,
|
||||||
|
"parent_device_id": "0000000000000000000000000000000000000000",
|
||||||
|
"region": "Europe/Berlin",
|
||||||
|
"report_interval": 16,
|
||||||
|
"rssi": -54,
|
||||||
|
"signal_level": 3,
|
||||||
|
"specs": "EU",
|
||||||
|
"status": "online",
|
||||||
|
"status_follow_edge": false,
|
||||||
|
"type": "SMART.TAPOSENSOR"
|
||||||
|
},
|
||||||
|
"get_fw_download_state": {
|
||||||
|
"cloud_cache_seconds": 1,
|
||||||
|
"download_progress": 30,
|
||||||
|
"reboot_time": 5,
|
||||||
|
"status": 4,
|
||||||
|
"upgrade_time": 5
|
||||||
|
},
|
||||||
|
"get_latest_fw": {
|
||||||
|
"fw_ver": "1.9.0 Build 230704 Rel.154531",
|
||||||
|
"hw_id": "00000000000000000000000000000000",
|
||||||
|
"need_to_upgrade": true,
|
||||||
|
"oem_id": "00000000000000000000000000000000",
|
||||||
|
"release_date": "2023-10-30",
|
||||||
|
"release_note": "Modifications and Bug Fixes:\n1. Reduced power consumption.\n2. Fixed some minor bugs.",
|
||||||
|
"type": 2
|
||||||
|
},
|
||||||
|
"get_temp_humidity_records": {
|
||||||
|
"local_time": 1714681046,
|
||||||
|
"past24h_humidity": [
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000
|
||||||
|
],
|
||||||
|
"past24h_humidity_exception": [
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000
|
||||||
|
],
|
||||||
|
"past24h_temp": [
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000
|
||||||
|
],
|
||||||
|
"past24h_temp_exception": [
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000,
|
||||||
|
-1000
|
||||||
|
],
|
||||||
|
"temp_unit": "celsius"
|
||||||
|
},
|
||||||
|
"get_trigger_logs": {
|
||||||
|
"logs": [
|
||||||
|
{
|
||||||
|
"event": "close",
|
||||||
|
"eventId": "8140289c-c66b-bdd6-63b9-542299442299",
|
||||||
|
"id": 4,
|
||||||
|
"timestamp": 1714661714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event": "open",
|
||||||
|
"eventId": "fb4e1439-2f2c-a5e1-c35a-9e7c0d35a1e3",
|
||||||
|
"id": 3,
|
||||||
|
"timestamp": 1714661710
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event": "close",
|
||||||
|
"eventId": "ddee7733-1180-48ac-56a3-512018048ac5",
|
||||||
|
"id": 2,
|
||||||
|
"timestamp": 1714661657
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event": "open",
|
||||||
|
"eventId": "ab80951f-da38-49f9-21c5-bf025c7b606d",
|
||||||
|
"id": 1,
|
||||||
|
"timestamp": 1714661638
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_id": 4,
|
||||||
|
"sum": 4
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@ import pytest
|
|||||||
|
|
||||||
from kasa.iot import IotDevice
|
from kasa.iot import IotDevice
|
||||||
from kasa.smart import SmartDevice
|
from kasa.smart import SmartDevice
|
||||||
from kasa.tests.conftest import dimmable, parametrize
|
from kasa.tests.conftest import dimmable_iot, parametrize
|
||||||
|
|
||||||
brightness = parametrize("brightness smart", component_filter="brightness")
|
brightness = parametrize("brightness smart", component_filter="brightness")
|
||||||
|
|
||||||
@ -10,13 +10,13 @@ brightness = parametrize("brightness smart", component_filter="brightness")
|
|||||||
@brightness
|
@brightness
|
||||||
async def test_brightness_component(dev: SmartDevice):
|
async def test_brightness_component(dev: SmartDevice):
|
||||||
"""Test brightness feature."""
|
"""Test brightness feature."""
|
||||||
brightness = dev.get_module("Brightness")
|
brightness = dev.modules.get("Brightness")
|
||||||
assert brightness
|
assert brightness
|
||||||
assert isinstance(dev, SmartDevice)
|
assert isinstance(dev, SmartDevice)
|
||||||
assert "brightness" in dev._components
|
assert "brightness" in dev._components
|
||||||
|
|
||||||
# Test getting the value
|
# Test getting the value
|
||||||
feature = brightness._module_features["brightness"]
|
feature = dev.features["brightness"]
|
||||||
assert isinstance(feature.value, int)
|
assert isinstance(feature.value, int)
|
||||||
assert feature.value > 1 and feature.value <= 100
|
assert feature.value > 1 and feature.value <= 100
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ async def test_brightness_component(dev: SmartDevice):
|
|||||||
await feature.set_value(feature.maximum_value + 10)
|
await feature.set_value(feature.maximum_value + 10)
|
||||||
|
|
||||||
|
|
||||||
@dimmable
|
@dimmable_iot
|
||||||
async def test_brightness_dimmable(dev: IotDevice):
|
async def test_brightness_dimmable(dev: IotDevice):
|
||||||
"""Test brightness feature."""
|
"""Test brightness feature."""
|
||||||
assert isinstance(dev, IotDevice)
|
assert isinstance(dev, IotDevice)
|
||||||
|
28
kasa/tests/smart/modules/test_contact.py
Normal file
28
kasa/tests/smart/modules/test_contact.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from kasa import Module, SmartDevice
|
||||||
|
from kasa.tests.device_fixtures import parametrize
|
||||||
|
|
||||||
|
contact = parametrize(
|
||||||
|
"is contact sensor", model_filter="T110", protocol_filter={"SMART.CHILD"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contact
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"feature, type",
|
||||||
|
[
|
||||||
|
("is_open", bool),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_contact_features(dev: SmartDevice, feature, type):
|
||||||
|
"""Test that features are registered and work as expected."""
|
||||||
|
contact = dev.modules.get(Module.ContactSensor)
|
||||||
|
assert contact is not None
|
||||||
|
|
||||||
|
prop = getattr(contact, feature)
|
||||||
|
assert isinstance(prop, type)
|
||||||
|
|
||||||
|
feat = dev.features[feature]
|
||||||
|
assert feat.value == prop
|
||||||
|
assert isinstance(feat.value, type)
|
@ -1,8 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from kasa import Module
|
||||||
from kasa.smart import SmartDevice
|
from kasa.smart import SmartDevice
|
||||||
from kasa.smart.modules import FanModule
|
|
||||||
from kasa.tests.device_fixtures import parametrize
|
from kasa.tests.device_fixtures import parametrize
|
||||||
|
|
||||||
fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"SMART"})
|
fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"SMART"})
|
||||||
@ -11,10 +11,10 @@ fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"S
|
|||||||
@fan
|
@fan
|
||||||
async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
|
async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
|
||||||
"""Test fan speed feature."""
|
"""Test fan speed feature."""
|
||||||
fan = dev.get_module(FanModule)
|
fan = dev.modules.get(Module.Fan)
|
||||||
assert fan
|
assert fan
|
||||||
|
|
||||||
level_feature = fan._module_features["fan_speed_level"]
|
level_feature = dev.features["fan_speed_level"]
|
||||||
assert (
|
assert (
|
||||||
level_feature.minimum_value
|
level_feature.minimum_value
|
||||||
<= level_feature.value
|
<= level_feature.value
|
||||||
@ -36,9 +36,9 @@ async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
|
|||||||
@fan
|
@fan
|
||||||
async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
|
async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
|
||||||
"""Test sleep mode feature."""
|
"""Test sleep mode feature."""
|
||||||
fan = dev.get_module(FanModule)
|
fan = dev.modules.get(Module.Fan)
|
||||||
assert fan
|
assert fan
|
||||||
sleep_feature = fan._module_features["fan_sleep_mode"]
|
sleep_feature = dev.features["fan_sleep_mode"]
|
||||||
assert isinstance(sleep_feature.value, bool)
|
assert isinstance(sleep_feature.value, bool)
|
||||||
|
|
||||||
call = mocker.spy(fan, "call")
|
call = mocker.spy(fan, "call")
|
||||||
@ -52,29 +52,28 @@ async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
|
|||||||
|
|
||||||
|
|
||||||
@fan
|
@fan
|
||||||
async def test_fan_interface(dev: SmartDevice, mocker: MockerFixture):
|
async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
|
||||||
"""Test fan speed on device interface."""
|
"""Test fan speed on device interface."""
|
||||||
assert isinstance(dev, SmartDevice)
|
assert isinstance(dev, SmartDevice)
|
||||||
fan = dev.get_module(FanModule)
|
fan = dev.modules.get(Module.Fan)
|
||||||
assert fan
|
assert fan
|
||||||
device = fan._device
|
device = fan._device
|
||||||
assert device.is_fan
|
|
||||||
|
|
||||||
await device.set_fan_speed_level(1)
|
await fan.set_fan_speed_level(1)
|
||||||
await dev.update()
|
await dev.update()
|
||||||
assert device.fan_speed_level == 1
|
assert fan.fan_speed_level == 1
|
||||||
assert device.is_on
|
assert device.is_on
|
||||||
|
|
||||||
await device.set_fan_speed_level(4)
|
await fan.set_fan_speed_level(4)
|
||||||
await dev.update()
|
await dev.update()
|
||||||
assert device.fan_speed_level == 4
|
assert fan.fan_speed_level == 4
|
||||||
|
|
||||||
await device.set_fan_speed_level(0)
|
await fan.set_fan_speed_level(0)
|
||||||
await dev.update()
|
await dev.update()
|
||||||
assert not device.is_on
|
assert not device.is_on
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await device.set_fan_speed_level(-1)
|
await fan.set_fan_speed_level(-1)
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await device.set_fan_speed_level(5)
|
await fan.set_fan_speed_level(5)
|
||||||
|
113
kasa/tests/smart/modules/test_firmware.py
Normal file
113
kasa/tests/smart/modules/test_firmware.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from kasa import Module
|
||||||
|
from kasa.smart import SmartDevice
|
||||||
|
from kasa.smart.modules.firmware import DownloadState, Firmware
|
||||||
|
from kasa.tests.device_fixtures import parametrize
|
||||||
|
|
||||||
|
firmware = parametrize(
|
||||||
|
"has firmware", component_filter="firmware", protocol_filter={"SMART"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@firmware
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"feature, prop_name, type, required_version",
|
||||||
|
[
|
||||||
|
("auto_update_enabled", "auto_update_enabled", bool, 2),
|
||||||
|
("update_available", "update_available", bool, 1),
|
||||||
|
("update_available", "update_available", bool, 1),
|
||||||
|
("current_firmware_version", "current_firmware", str, 1),
|
||||||
|
("available_firmware_version", "latest_firmware", str, 1),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_firmware_features(
|
||||||
|
dev: SmartDevice, feature, prop_name, type, required_version, mocker: MockerFixture
|
||||||
|
):
|
||||||
|
"""Test light effect."""
|
||||||
|
fw = dev.modules.get(Module.Firmware)
|
||||||
|
assert fw
|
||||||
|
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
|
||||||
|
return
|
||||||
|
if not dev.is_cloud_connected:
|
||||||
|
pytest.skip("Device is not cloud connected, skipping test")
|
||||||
|
|
||||||
|
if fw.supported_version < required_version:
|
||||||
|
pytest.skip("Feature %s requires newer version" % feature)
|
||||||
|
|
||||||
|
prop = getattr(fw, prop_name)
|
||||||
|
assert isinstance(prop, type)
|
||||||
|
|
||||||
|
feat = dev.features[feature]
|
||||||
|
assert feat.value == prop
|
||||||
|
assert isinstance(feat.value, type)
|
||||||
|
|
||||||
|
|
||||||
|
@firmware
|
||||||
|
async def test_update_available_without_cloud(dev: SmartDevice):
|
||||||
|
"""Test that update_available returns None when disconnected."""
|
||||||
|
fw = dev.modules.get(Module.Firmware)
|
||||||
|
assert fw
|
||||||
|
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
|
||||||
|
return
|
||||||
|
|
||||||
|
if dev.is_cloud_connected:
|
||||||
|
assert isinstance(fw.update_available, bool)
|
||||||
|
else:
|
||||||
|
assert fw.update_available is None
|
||||||
|
|
||||||
|
|
||||||
|
@firmware
|
||||||
|
async def test_firmware_update(
|
||||||
|
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
|
||||||
|
):
|
||||||
|
"""Test updating firmware."""
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
fw = dev.modules.get(Module.Firmware)
|
||||||
|
assert fw
|
||||||
|
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
|
||||||
|
return
|
||||||
|
|
||||||
|
upgrade_time = 5
|
||||||
|
extras = {"reboot_time": 5, "upgrade_time": upgrade_time, "auto_upgrade": False}
|
||||||
|
update_states = [
|
||||||
|
# Unknown 1
|
||||||
|
DownloadState(status=1, download_progress=0, **extras),
|
||||||
|
# Downloading
|
||||||
|
DownloadState(status=2, download_progress=10, **extras),
|
||||||
|
DownloadState(status=2, download_progress=100, **extras),
|
||||||
|
# Flashing
|
||||||
|
DownloadState(status=3, download_progress=100, **extras),
|
||||||
|
DownloadState(status=3, download_progress=100, **extras),
|
||||||
|
# Done
|
||||||
|
DownloadState(status=0, download_progress=100, **extras),
|
||||||
|
]
|
||||||
|
|
||||||
|
asyncio_sleep = asyncio.sleep
|
||||||
|
sleep = mocker.patch("asyncio.sleep")
|
||||||
|
mocker.patch.object(fw, "get_update_state", side_effect=update_states)
|
||||||
|
|
||||||
|
cb_mock = mocker.AsyncMock()
|
||||||
|
|
||||||
|
await fw.update(progress_cb=cb_mock)
|
||||||
|
|
||||||
|
# This is necessary to allow the eventloop to process the created tasks
|
||||||
|
await asyncio_sleep(0)
|
||||||
|
|
||||||
|
assert "Unhandled state code" in caplog.text
|
||||||
|
assert "Downloading firmware, progress: 10" in caplog.text
|
||||||
|
assert "Flashing firmware, sleeping" in caplog.text
|
||||||
|
assert "Update idle" in caplog.text
|
||||||
|
|
||||||
|
for state in update_states:
|
||||||
|
cb_mock.assert_any_await(state)
|
||||||
|
|
||||||
|
# sleep based on the upgrade_time
|
||||||
|
sleep.assert_any_call(upgrade_time)
|
@ -23,6 +23,6 @@ async def test_humidity_features(dev, feature, type):
|
|||||||
prop = getattr(humidity, feature)
|
prop = getattr(humidity, feature)
|
||||||
assert isinstance(prop, type)
|
assert isinstance(prop, type)
|
||||||
|
|
||||||
feat = humidity._module_features[feature]
|
feat = dev.features[feature]
|
||||||
assert feat.value == prop
|
assert feat.value == prop
|
||||||
assert isinstance(feat.value, type)
|
assert isinstance(feat.value, type)
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from kasa import Device, Feature
|
from kasa import Device, Feature, Module
|
||||||
from kasa.smart.modules import LightEffectModule
|
from kasa.smart.modules import LightEffect
|
||||||
from kasa.tests.device_fixtures import parametrize
|
from kasa.tests.device_fixtures import parametrize
|
||||||
|
|
||||||
light_effect = parametrize(
|
light_effect = parametrize(
|
||||||
@ -18,10 +17,10 @@ light_effect = parametrize(
|
|||||||
@light_effect
|
@light_effect
|
||||||
async def test_light_effect(dev: Device, mocker: MockerFixture):
|
async def test_light_effect(dev: Device, mocker: MockerFixture):
|
||||||
"""Test light effect."""
|
"""Test light effect."""
|
||||||
light_effect = cast(LightEffectModule, dev.modules.get("LightEffectModule"))
|
light_effect = dev.modules.get(Module.LightEffect)
|
||||||
assert light_effect
|
assert isinstance(light_effect, LightEffect)
|
||||||
|
|
||||||
feature = light_effect._module_features["light_effect"]
|
feature = dev.features["light_effect"]
|
||||||
assert feature.type == Feature.Type.Choice
|
assert feature.type == Feature.Type.Choice
|
||||||
|
|
||||||
call = mocker.spy(light_effect, "call")
|
call = mocker.spy(light_effect, "call")
|
||||||
@ -29,7 +28,7 @@ async def test_light_effect(dev: Device, mocker: MockerFixture):
|
|||||||
assert feature.choices
|
assert feature.choices
|
||||||
for effect in chain(reversed(feature.choices), feature.choices):
|
for effect in chain(reversed(feature.choices), feature.choices):
|
||||||
await light_effect.set_effect(effect)
|
await light_effect.set_effect(effect)
|
||||||
enable = effect != LightEffectModule.LIGHT_EFFECTS_OFF
|
enable = effect != LightEffect.LIGHT_EFFECTS_OFF
|
||||||
params: dict[str, bool | str] = {"enable": enable}
|
params: dict[str, bool | str] = {"enable": enable}
|
||||||
if enable:
|
if enable:
|
||||||
params["id"] = light_effect._scenes_names_to_id[effect]
|
params["id"] = light_effect._scenes_names_to_id[effect]
|
||||||
|
@ -29,7 +29,7 @@ async def test_temperature_features(dev, feature, type):
|
|||||||
prop = getattr(temp_module, feature)
|
prop = getattr(temp_module, feature)
|
||||||
assert isinstance(prop, type)
|
assert isinstance(prop, type)
|
||||||
|
|
||||||
feat = temp_module._module_features[feature]
|
feat = dev.features[feature]
|
||||||
assert feat.value == prop
|
assert feat.value == prop
|
||||||
assert isinstance(feat.value, type)
|
assert isinstance(feat.value, type)
|
||||||
|
|
||||||
@ -42,6 +42,6 @@ async def test_temperature_warning(dev):
|
|||||||
assert hasattr(temp_module, "temperature_warning")
|
assert hasattr(temp_module, "temperature_warning")
|
||||||
assert isinstance(temp_module.temperature_warning, bool)
|
assert isinstance(temp_module.temperature_warning, bool)
|
||||||
|
|
||||||
feat = temp_module._module_features["temperature_warning"]
|
feat = dev.features["temperature_warning"]
|
||||||
assert feat.value == temp_module.temperature_warning
|
assert feat.value == temp_module.temperature_warning
|
||||||
assert isinstance(feat.value, bool)
|
assert isinstance(feat.value, bool)
|
||||||
|
@ -28,7 +28,7 @@ async def test_temperature_control_features(dev, feature, type):
|
|||||||
prop = getattr(temp_module, feature)
|
prop = getattr(temp_module, feature)
|
||||||
assert isinstance(prop, type)
|
assert isinstance(prop, type)
|
||||||
|
|
||||||
feat = temp_module._module_features[feature]
|
feat = dev.features[feature]
|
||||||
assert feat.value == prop
|
assert feat.value == prop
|
||||||
assert isinstance(feat.value, type)
|
assert isinstance(feat.value, type)
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ async def test_waterleak_properties(dev, feature, prop_name, type):
|
|||||||
prop = getattr(waterleak, prop_name)
|
prop = getattr(waterleak, prop_name)
|
||||||
assert isinstance(prop, type)
|
assert isinstance(prop, type)
|
||||||
|
|
||||||
feat = waterleak._module_features[feature]
|
feat = dev.features[feature]
|
||||||
assert feat.value == prop
|
assert feat.value == prop
|
||||||
assert isinstance(feat.value, type)
|
assert isinstance(feat.value, type)
|
||||||
|
|
||||||
|
@ -7,19 +7,18 @@ from voluptuous import (
|
|||||||
Schema,
|
Schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
from kasa import Bulb, BulbPreset, Device, DeviceType, KasaException
|
from kasa import Device, DeviceType, KasaException, LightPreset, Module
|
||||||
from kasa.iot import IotBulb, IotDimmer
|
from kasa.iot import IotBulb, IotDimmer
|
||||||
from kasa.smart import SmartDevice
|
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
bulb,
|
bulb,
|
||||||
bulb_iot,
|
bulb_iot,
|
||||||
color_bulb,
|
color_bulb,
|
||||||
color_bulb_iot,
|
color_bulb_iot,
|
||||||
dimmable,
|
dimmable_iot,
|
||||||
handle_turn_on,
|
handle_turn_on,
|
||||||
non_color_bulb,
|
non_color_bulb,
|
||||||
non_dimmable,
|
non_dimmable_iot,
|
||||||
non_variable_temp,
|
non_variable_temp,
|
||||||
turn_on,
|
turn_on,
|
||||||
variable_temp,
|
variable_temp,
|
||||||
@ -65,19 +64,20 @@ async def test_get_light_state(dev: IotBulb):
|
|||||||
@color_bulb
|
@color_bulb
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_hsv(dev: Device, turn_on):
|
async def test_hsv(dev: Device, turn_on):
|
||||||
assert isinstance(dev, Bulb)
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
assert dev.is_color
|
assert light.is_color
|
||||||
|
|
||||||
hue, saturation, brightness = dev.hsv
|
hue, saturation, brightness = light.hsv
|
||||||
assert 0 <= hue <= 360
|
assert 0 <= hue <= 360
|
||||||
assert 0 <= saturation <= 100
|
assert 0 <= saturation <= 100
|
||||||
assert 0 <= brightness <= 100
|
assert 0 <= brightness <= 100
|
||||||
|
|
||||||
await dev.set_hsv(hue=1, saturation=1, value=1)
|
await light.set_hsv(hue=1, saturation=1, value=1)
|
||||||
|
|
||||||
await dev.update()
|
await dev.update()
|
||||||
hue, saturation, brightness = dev.hsv
|
hue, saturation, brightness = light.hsv
|
||||||
assert hue == 1
|
assert hue == 1
|
||||||
assert saturation == 1
|
assert saturation == 1
|
||||||
assert brightness == 1
|
assert brightness == 1
|
||||||
@ -96,57 +96,64 @@ async def test_set_hsv_transition(dev: IotBulb, mocker):
|
|||||||
|
|
||||||
@color_bulb
|
@color_bulb
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_invalid_hsv(dev: Bulb, turn_on):
|
async def test_invalid_hsv(dev: Device, turn_on):
|
||||||
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
assert dev.is_color
|
assert light.is_color
|
||||||
|
|
||||||
for invalid_hue in [-1, 361, 0.5]:
|
for invalid_hue in [-1, 361, 0.5]:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_hsv(invalid_hue, 0, 0) # type: ignore[arg-type]
|
await light.set_hsv(invalid_hue, 0, 0) # type: ignore[arg-type]
|
||||||
|
|
||||||
for invalid_saturation in [-1, 101, 0.5]:
|
for invalid_saturation in [-1, 101, 0.5]:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_hsv(0, invalid_saturation, 0) # type: ignore[arg-type]
|
await light.set_hsv(0, invalid_saturation, 0) # type: ignore[arg-type]
|
||||||
|
|
||||||
for invalid_brightness in [-1, 101, 0.5]:
|
for invalid_brightness in [-1, 101, 0.5]:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_hsv(0, 0, invalid_brightness) # type: ignore[arg-type]
|
await light.set_hsv(0, 0, invalid_brightness) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
@color_bulb
|
@color_bulb
|
||||||
@pytest.mark.skip("requires color feature")
|
@pytest.mark.skip("requires color feature")
|
||||||
async def test_color_state_information(dev: Device):
|
async def test_color_state_information(dev: Device):
|
||||||
assert isinstance(dev, Bulb)
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
assert "HSV" in dev.state_information
|
assert "HSV" in dev.state_information
|
||||||
assert dev.state_information["HSV"] == dev.hsv
|
assert dev.state_information["HSV"] == light.hsv
|
||||||
|
|
||||||
|
|
||||||
@non_color_bulb
|
@non_color_bulb
|
||||||
async def test_hsv_on_non_color(dev: Bulb):
|
async def test_hsv_on_non_color(dev: Device):
|
||||||
assert not dev.is_color
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
|
assert not light.is_color
|
||||||
|
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
await dev.set_hsv(0, 0, 0)
|
await light.set_hsv(0, 0, 0)
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
print(dev.hsv)
|
print(light.hsv)
|
||||||
|
|
||||||
|
|
||||||
@variable_temp
|
@variable_temp
|
||||||
@pytest.mark.skip("requires colortemp module")
|
@pytest.mark.skip("requires colortemp module")
|
||||||
async def test_variable_temp_state_information(dev: Device):
|
async def test_variable_temp_state_information(dev: Device):
|
||||||
assert isinstance(dev, Bulb)
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
assert "Color temperature" in dev.state_information
|
assert "Color temperature" in dev.state_information
|
||||||
assert dev.state_information["Color temperature"] == dev.color_temp
|
assert dev.state_information["Color temperature"] == light.color_temp
|
||||||
|
|
||||||
|
|
||||||
@variable_temp
|
@variable_temp
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_try_set_colortemp(dev: Device, turn_on):
|
async def test_try_set_colortemp(dev: Device, turn_on):
|
||||||
assert isinstance(dev, Bulb)
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
await dev.set_color_temp(2700)
|
await light.set_color_temp(2700)
|
||||||
await dev.update()
|
await dev.update()
|
||||||
assert dev.color_temp == 2700
|
assert light.color_temp == 2700
|
||||||
|
|
||||||
|
|
||||||
@variable_temp_iot
|
@variable_temp_iot
|
||||||
@ -166,36 +173,42 @@ async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
|
|||||||
|
|
||||||
|
|
||||||
@variable_temp_smart
|
@variable_temp_smart
|
||||||
async def test_smart_temp_range(dev: SmartDevice):
|
async def test_smart_temp_range(dev: Device):
|
||||||
assert dev.valid_temperature_range
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
|
assert light.valid_temperature_range
|
||||||
|
|
||||||
|
|
||||||
@variable_temp
|
@variable_temp
|
||||||
async def test_out_of_range_temperature(dev: Bulb):
|
async def test_out_of_range_temperature(dev: Device):
|
||||||
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_color_temp(1000)
|
await light.set_color_temp(1000)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_color_temp(10000)
|
await light.set_color_temp(10000)
|
||||||
|
|
||||||
|
|
||||||
@non_variable_temp
|
@non_variable_temp
|
||||||
async def test_non_variable_temp(dev: Bulb):
|
async def test_non_variable_temp(dev: Device):
|
||||||
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
await dev.set_color_temp(2700)
|
await light.set_color_temp(2700)
|
||||||
|
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
print(dev.valid_temperature_range)
|
print(light.valid_temperature_range)
|
||||||
|
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
print(dev.color_temp)
|
print(light.color_temp)
|
||||||
|
|
||||||
|
|
||||||
@dimmable
|
@dimmable_iot
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_dimmable_brightness(dev: Device, turn_on):
|
async def test_dimmable_brightness(dev: IotBulb, turn_on):
|
||||||
assert isinstance(dev, (Bulb, IotDimmer))
|
assert isinstance(dev, (IotBulb, IotDimmer))
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
assert dev.is_dimmable
|
assert dev._is_dimmable
|
||||||
|
|
||||||
await dev.set_brightness(50)
|
await dev.set_brightness(50)
|
||||||
await dev.update()
|
await dev.update()
|
||||||
@ -229,9 +242,9 @@ async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
|
|||||||
set_light_state.assert_called_with({"brightness": 10}, transition=1000)
|
set_light_state.assert_called_with({"brightness": 10}, transition=1000)
|
||||||
|
|
||||||
|
|
||||||
@dimmable
|
@dimmable_iot
|
||||||
async def test_invalid_brightness(dev: Bulb):
|
async def test_invalid_brightness(dev: IotBulb):
|
||||||
assert dev.is_dimmable
|
assert dev._is_dimmable
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_brightness(110)
|
await dev.set_brightness(110)
|
||||||
@ -240,9 +253,9 @@ async def test_invalid_brightness(dev: Bulb):
|
|||||||
await dev.set_brightness(-100)
|
await dev.set_brightness(-100)
|
||||||
|
|
||||||
|
|
||||||
@non_dimmable
|
@non_dimmable_iot
|
||||||
async def test_non_dimmable(dev: Bulb):
|
async def test_non_dimmable(dev: IotBulb):
|
||||||
assert not dev.is_dimmable
|
assert not dev._is_dimmable
|
||||||
|
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
assert dev.brightness == 0
|
assert dev.brightness == 0
|
||||||
@ -291,7 +304,7 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
|||||||
"saturation": 0,
|
"saturation": 0,
|
||||||
"color_temp": 0,
|
"color_temp": 0,
|
||||||
}
|
}
|
||||||
preset = BulbPreset(**data)
|
preset = LightPreset(**data)
|
||||||
|
|
||||||
assert preset.index == 0
|
assert preset.index == 0
|
||||||
assert preset.brightness == 10
|
assert preset.brightness == 10
|
||||||
@ -305,7 +318,7 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
|||||||
|
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(KasaException):
|
||||||
await dev.save_preset(
|
await dev.save_preset(
|
||||||
BulbPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
|
LightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -314,11 +327,11 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
|||||||
("preset", "payload"),
|
("preset", "payload"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
BulbPreset(index=0, hue=0, brightness=1, saturation=0),
|
LightPreset(index=0, hue=0, brightness=1, saturation=0),
|
||||||
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
|
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
BulbPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
|
LightPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
|
||||||
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
|
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -380,7 +393,7 @@ SYSINFO_SCHEMA_BULB = SYSINFO_SCHEMA.extend(
|
|||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb
|
||||||
def test_device_type_bulb(dev):
|
def test_device_type_bulb(dev: Device):
|
||||||
if dev.is_light_strip:
|
if dev.is_light_strip:
|
||||||
pytest.skip("bulb has also lightstrips to test the api")
|
pytest.skip("bulb has also lightstrips to test the api")
|
||||||
assert dev.device_type == DeviceType.Bulb
|
assert dev.device_type == DeviceType.Bulb
|
||||||
|
@ -13,6 +13,7 @@ from kasa import (
|
|||||||
DeviceError,
|
DeviceError,
|
||||||
EmeterStatus,
|
EmeterStatus,
|
||||||
KasaException,
|
KasaException,
|
||||||
|
Module,
|
||||||
UnsupportedDeviceError,
|
UnsupportedDeviceError,
|
||||||
)
|
)
|
||||||
from kasa.cli import (
|
from kasa.cli import (
|
||||||
@ -21,11 +22,15 @@ from kasa.cli import (
|
|||||||
brightness,
|
brightness,
|
||||||
cli,
|
cli,
|
||||||
cmd_command,
|
cmd_command,
|
||||||
|
effect,
|
||||||
emeter,
|
emeter,
|
||||||
|
hsv,
|
||||||
|
led,
|
||||||
raw_command,
|
raw_command,
|
||||||
reboot,
|
reboot,
|
||||||
state,
|
state,
|
||||||
sysinfo,
|
sysinfo,
|
||||||
|
temperature,
|
||||||
toggle,
|
toggle,
|
||||||
update_credentials,
|
update_credentials,
|
||||||
wifi,
|
wifi,
|
||||||
@ -34,7 +39,6 @@ from kasa.discover import Discover, DiscoveryResult
|
|||||||
from kasa.iot import IotDevice
|
from kasa.iot import IotDevice
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
device_iot,
|
|
||||||
device_smart,
|
device_smart,
|
||||||
get_device_for_fixture_protocol,
|
get_device_for_fixture_protocol,
|
||||||
handle_turn_on,
|
handle_turn_on,
|
||||||
@ -78,11 +82,10 @@ async def test_update_called_by_cli(dev, mocker, runner):
|
|||||||
update.assert_called()
|
update.assert_called()
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
async def test_sysinfo(dev: Device, runner):
|
||||||
async def test_sysinfo(dev, runner):
|
|
||||||
res = await runner.invoke(sysinfo, obj=dev)
|
res = await runner.invoke(sysinfo, obj=dev)
|
||||||
assert "System info" in res.output
|
assert "System info" in res.output
|
||||||
assert dev.alias in res.output
|
assert dev.model in res.output
|
||||||
|
|
||||||
|
|
||||||
@turn_on
|
@turn_on
|
||||||
@ -108,7 +111,6 @@ async def test_toggle(dev, turn_on, runner):
|
|||||||
assert dev.is_on != turn_on
|
assert dev.is_on != turn_on
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_alias(dev, runner):
|
async def test_alias(dev, runner):
|
||||||
res = await runner.invoke(alias, obj=dev)
|
res = await runner.invoke(alias, obj=dev)
|
||||||
assert f"Alias: {dev.alias}" in res.output
|
assert f"Alias: {dev.alias}" in res.output
|
||||||
@ -308,15 +310,14 @@ async def test_emeter(dev: Device, mocker, runner):
|
|||||||
daily.assert_called_with(year=1900, month=12)
|
daily.assert_called_with(year=1900, month=12)
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
async def test_brightness(dev: Device, runner):
|
||||||
async def test_brightness(dev, runner):
|
|
||||||
res = await runner.invoke(brightness, obj=dev)
|
res = await runner.invoke(brightness, obj=dev)
|
||||||
if not dev.is_dimmable:
|
if not (light := dev.modules.get(Module.Light)) or not light.is_dimmable:
|
||||||
assert "This device does not support brightness." in res.output
|
assert "This device does not support brightness." in res.output
|
||||||
return
|
return
|
||||||
|
|
||||||
res = await runner.invoke(brightness, obj=dev)
|
res = await runner.invoke(brightness, obj=dev)
|
||||||
assert f"Brightness: {dev.brightness}" in res.output
|
assert f"Brightness: {light.brightness}" in res.output
|
||||||
|
|
||||||
res = await runner.invoke(brightness, ["12"], obj=dev)
|
res = await runner.invoke(brightness, ["12"], obj=dev)
|
||||||
assert "Setting brightness" in res.output
|
assert "Setting brightness" in res.output
|
||||||
@ -326,7 +327,110 @@ async def test_brightness(dev, runner):
|
|||||||
assert "Brightness: 12" in res.output
|
assert "Brightness: 12" in res.output
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
async def test_color_temperature(dev: Device, runner):
|
||||||
|
res = await runner.invoke(temperature, obj=dev)
|
||||||
|
if not (light := dev.modules.get(Module.Light)) or not light.is_variable_color_temp:
|
||||||
|
assert "Device does not support color temperature" in res.output
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await runner.invoke(temperature, obj=dev)
|
||||||
|
assert f"Color temperature: {light.color_temp}" in res.output
|
||||||
|
valid_range = light.valid_temperature_range
|
||||||
|
assert f"(min: {valid_range.min}, max: {valid_range.max})" in res.output
|
||||||
|
|
||||||
|
val = int((valid_range.min + valid_range.max) / 2)
|
||||||
|
res = await runner.invoke(temperature, [str(val)], obj=dev)
|
||||||
|
assert "Setting color temperature to " in res.output
|
||||||
|
await dev.update()
|
||||||
|
|
||||||
|
res = await runner.invoke(temperature, obj=dev)
|
||||||
|
assert f"Color temperature: {val}" in res.output
|
||||||
|
assert res.exit_code == 0
|
||||||
|
|
||||||
|
invalid_max = valid_range.max + 100
|
||||||
|
# Lights that support the maximum range will not get past the click cli range check
|
||||||
|
# So can't be tested for the internal range check.
|
||||||
|
if invalid_max < 9000:
|
||||||
|
res = await runner.invoke(temperature, [str(invalid_max)], obj=dev)
|
||||||
|
assert res.exit_code == 1
|
||||||
|
assert isinstance(res.exception, ValueError)
|
||||||
|
|
||||||
|
res = await runner.invoke(temperature, [str(9100)], obj=dev)
|
||||||
|
assert res.exit_code == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_color_hsv(dev: Device, runner: CliRunner):
|
||||||
|
res = await runner.invoke(hsv, obj=dev)
|
||||||
|
if not (light := dev.modules.get(Module.Light)) or not light.is_color:
|
||||||
|
assert "Device does not support colors" in res.output
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await runner.invoke(hsv, obj=dev)
|
||||||
|
assert f"Current HSV: {light.hsv}" in res.output
|
||||||
|
|
||||||
|
res = await runner.invoke(hsv, ["180", "50", "50"], obj=dev)
|
||||||
|
assert "Setting HSV: 180 50 50" in res.output
|
||||||
|
assert res.exit_code == 0
|
||||||
|
await dev.update()
|
||||||
|
|
||||||
|
res = await runner.invoke(hsv, ["180", "50"], obj=dev)
|
||||||
|
assert "Setting a color requires 3 values." in res.output
|
||||||
|
assert res.exit_code == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_light_effect(dev: Device, runner: CliRunner):
|
||||||
|
res = await runner.invoke(effect, obj=dev)
|
||||||
|
if not (light_effect := dev.modules.get(Module.LightEffect)):
|
||||||
|
assert "Device does not support effects" in res.output
|
||||||
|
return
|
||||||
|
|
||||||
|
# Start off with a known state of off
|
||||||
|
await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF)
|
||||||
|
await dev.update()
|
||||||
|
assert light_effect.effect == light_effect.LIGHT_EFFECTS_OFF
|
||||||
|
|
||||||
|
res = await runner.invoke(effect, obj=dev)
|
||||||
|
msg = (
|
||||||
|
"Setting an effect requires a named built-in effect: "
|
||||||
|
+ f"{light_effect.effect_list}"
|
||||||
|
)
|
||||||
|
assert msg in res.output
|
||||||
|
assert res.exit_code == 2
|
||||||
|
|
||||||
|
res = await runner.invoke(effect, [light_effect.effect_list[1]], obj=dev)
|
||||||
|
assert f"Setting Effect: {light_effect.effect_list[1]}" in res.output
|
||||||
|
assert res.exit_code == 0
|
||||||
|
await dev.update()
|
||||||
|
assert light_effect.effect == light_effect.effect_list[1]
|
||||||
|
|
||||||
|
res = await runner.invoke(effect, ["foobar"], obj=dev)
|
||||||
|
assert f"Effect must be one of: {light_effect.effect_list}" in res.output
|
||||||
|
assert res.exit_code == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_led(dev: Device, runner: CliRunner):
|
||||||
|
res = await runner.invoke(led, obj=dev)
|
||||||
|
if not (led_module := dev.modules.get(Module.Led)):
|
||||||
|
assert "Device does not support led" in res.output
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await runner.invoke(led, obj=dev)
|
||||||
|
assert f"LED state: {led_module.led}" in res.output
|
||||||
|
assert res.exit_code == 0
|
||||||
|
|
||||||
|
res = await runner.invoke(led, ["on"], obj=dev)
|
||||||
|
assert "Turning led to True" in res.output
|
||||||
|
assert res.exit_code == 0
|
||||||
|
await dev.update()
|
||||||
|
assert led_module.led is True
|
||||||
|
|
||||||
|
res = await runner.invoke(led, ["off"], obj=dev)
|
||||||
|
assert "Turning led to False" in res.output
|
||||||
|
assert res.exit_code == 0
|
||||||
|
await dev.update()
|
||||||
|
assert led_module.led is False
|
||||||
|
|
||||||
|
|
||||||
async def test_json_output(dev: Device, mocker, runner):
|
async def test_json_output(dev: Device, mocker, runner):
|
||||||
"""Test that the json output produces correct output."""
|
"""Test that the json output produces correct output."""
|
||||||
mocker.patch("kasa.Discover.discover", return_value={"127.0.0.1": dev})
|
mocker.patch("kasa.Discover.discover", return_value={"127.0.0.1": dev})
|
||||||
@ -375,7 +479,6 @@ async def test_credentials(discovery_mock, mocker, runner):
|
|||||||
assert "Username:foo Password:bar\n" in res.output
|
assert "Username:foo Password:bar\n" in res.output
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_without_device_type(dev, mocker, runner):
|
async def test_without_device_type(dev, mocker, runner):
|
||||||
"""Test connecting without the device type."""
|
"""Test connecting without the device type."""
|
||||||
discovery_mock = mocker.patch(
|
discovery_mock = mocker.patch(
|
||||||
@ -737,7 +840,7 @@ async def test_feature_set(mocker, runner):
|
|||||||
dummy_device = await get_device_for_fixture_protocol(
|
dummy_device = await get_device_for_fixture_protocol(
|
||||||
"P300(EU)_1.0_1.0.13.json", "SMART"
|
"P300(EU)_1.0_1.0.13.json", "SMART"
|
||||||
)
|
)
|
||||||
led_setter = mocker.patch("kasa.smart.modules.ledmodule.LedModule.set_led")
|
led_setter = mocker.patch("kasa.smart.modules.led.Led.set_led")
|
||||||
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
|
mocker.patch("kasa.discover.Discover.discover_single", return_value=dummy_device)
|
||||||
|
|
||||||
res = await runner.invoke(
|
res = await runner.invoke(
|
||||||
|
125
kasa/tests/test_common_modules.py
Normal file
125
kasa/tests/test_common_modules.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import pytest
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from kasa import Device, Module
|
||||||
|
from kasa.tests.device_fixtures import (
|
||||||
|
dimmable_iot,
|
||||||
|
dimmer_iot,
|
||||||
|
lightstrip_iot,
|
||||||
|
parametrize,
|
||||||
|
parametrize_combine,
|
||||||
|
plug_iot,
|
||||||
|
)
|
||||||
|
|
||||||
|
led_smart = parametrize(
|
||||||
|
"has led smart", component_filter="led", protocol_filter={"SMART"}
|
||||||
|
)
|
||||||
|
led = parametrize_combine([led_smart, plug_iot])
|
||||||
|
|
||||||
|
light_effect_smart = parametrize(
|
||||||
|
"has light effect smart", component_filter="light_effect", protocol_filter={"SMART"}
|
||||||
|
)
|
||||||
|
light_effect = parametrize_combine([light_effect_smart, lightstrip_iot])
|
||||||
|
|
||||||
|
dimmable_smart = parametrize(
|
||||||
|
"dimmable smart", component_filter="brightness", protocol_filter={"SMART"}
|
||||||
|
)
|
||||||
|
dimmable = parametrize_combine([dimmable_smart, dimmer_iot, dimmable_iot])
|
||||||
|
|
||||||
|
|
||||||
|
@led
|
||||||
|
async def test_led_module(dev: Device, mocker: MockerFixture):
|
||||||
|
"""Test fan speed feature."""
|
||||||
|
led_module = dev.modules.get(Module.Led)
|
||||||
|
assert led_module
|
||||||
|
feat = dev.features["led"]
|
||||||
|
|
||||||
|
call = mocker.spy(led_module, "call")
|
||||||
|
await led_module.set_led(True)
|
||||||
|
assert call.call_count == 1
|
||||||
|
await dev.update()
|
||||||
|
assert led_module.led is True
|
||||||
|
assert feat.value is True
|
||||||
|
|
||||||
|
await led_module.set_led(False)
|
||||||
|
assert call.call_count == 2
|
||||||
|
await dev.update()
|
||||||
|
assert led_module.led is False
|
||||||
|
assert feat.value is False
|
||||||
|
|
||||||
|
await feat.set_value(True)
|
||||||
|
assert call.call_count == 3
|
||||||
|
await dev.update()
|
||||||
|
assert feat.value is True
|
||||||
|
assert led_module.led is True
|
||||||
|
|
||||||
|
|
||||||
|
@light_effect
|
||||||
|
async def test_light_effect_module(dev: Device, mocker: MockerFixture):
|
||||||
|
"""Test fan speed feature."""
|
||||||
|
light_effect_module = dev.modules[Module.LightEffect]
|
||||||
|
assert light_effect_module
|
||||||
|
feat = dev.features["light_effect"]
|
||||||
|
|
||||||
|
call = mocker.spy(light_effect_module, "call")
|
||||||
|
effect_list = light_effect_module.effect_list
|
||||||
|
assert "Off" in effect_list
|
||||||
|
assert effect_list.index("Off") == 0
|
||||||
|
assert len(effect_list) > 1
|
||||||
|
assert effect_list == feat.choices
|
||||||
|
|
||||||
|
assert light_effect_module.has_custom_effects is not None
|
||||||
|
|
||||||
|
await light_effect_module.set_effect("Off")
|
||||||
|
assert call.call_count == 1
|
||||||
|
await dev.update()
|
||||||
|
assert light_effect_module.effect == "Off"
|
||||||
|
assert feat.value == "Off"
|
||||||
|
|
||||||
|
second_effect = effect_list[1]
|
||||||
|
await light_effect_module.set_effect(second_effect)
|
||||||
|
assert call.call_count == 2
|
||||||
|
await dev.update()
|
||||||
|
assert light_effect_module.effect == second_effect
|
||||||
|
assert feat.value == second_effect
|
||||||
|
|
||||||
|
last_effect = effect_list[len(effect_list) - 1]
|
||||||
|
await light_effect_module.set_effect(last_effect)
|
||||||
|
assert call.call_count == 3
|
||||||
|
await dev.update()
|
||||||
|
assert light_effect_module.effect == last_effect
|
||||||
|
assert feat.value == last_effect
|
||||||
|
|
||||||
|
# Test feature set
|
||||||
|
await feat.set_value(second_effect)
|
||||||
|
assert call.call_count == 4
|
||||||
|
await dev.update()
|
||||||
|
assert light_effect_module.effect == second_effect
|
||||||
|
assert feat.value == second_effect
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await light_effect_module.set_effect("foobar")
|
||||||
|
assert call.call_count == 4
|
||||||
|
|
||||||
|
|
||||||
|
@dimmable
|
||||||
|
async def test_light_brightness(dev: Device):
|
||||||
|
"""Test brightness setter and getter."""
|
||||||
|
assert isinstance(dev, Device)
|
||||||
|
light = dev.modules.get(Module.Light)
|
||||||
|
assert light
|
||||||
|
|
||||||
|
# Test getting the value
|
||||||
|
feature = dev.features["brightness"]
|
||||||
|
assert feature.minimum_value == 0
|
||||||
|
assert feature.maximum_value == 100
|
||||||
|
|
||||||
|
await light.set_brightness(10)
|
||||||
|
await dev.update()
|
||||||
|
assert light.brightness == 10
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await light.set_brightness(feature.minimum_value - 10)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await light.set_brightness(feature.maximum_value + 10)
|
@ -9,7 +9,7 @@ from unittest.mock import Mock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import kasa
|
import kasa
|
||||||
from kasa import Credentials, Device, DeviceConfig
|
from kasa import Credentials, Device, DeviceConfig, DeviceType
|
||||||
from kasa.iot import IotDevice
|
from kasa.iot import IotDevice
|
||||||
from kasa.smart import SmartChildDevice, SmartDevice
|
from kasa.smart import SmartChildDevice, SmartDevice
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ def _get_subclasses(of_class):
|
|||||||
inspect.isclass(obj)
|
inspect.isclass(obj)
|
||||||
and issubclass(obj, of_class)
|
and issubclass(obj, of_class)
|
||||||
and module.__package__ != "kasa"
|
and module.__package__ != "kasa"
|
||||||
|
and module.__package__ != "kasa.interfaces"
|
||||||
):
|
):
|
||||||
subclasses.add((module.__package__ + "." + name, obj))
|
subclasses.add((module.__package__ + "." + name, obj))
|
||||||
return subclasses
|
return subclasses
|
||||||
@ -120,3 +121,56 @@ def test_deprecated_exceptions(exceptions_class, use_class):
|
|||||||
with pytest.deprecated_call(match=msg):
|
with pytest.deprecated_call(match=msg):
|
||||||
getattr(kasa, exceptions_class)
|
getattr(kasa, exceptions_class)
|
||||||
getattr(kasa, use_class.__name__)
|
getattr(kasa, use_class.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
deprecated_is_device_type = {
|
||||||
|
"is_bulb": DeviceType.Bulb,
|
||||||
|
"is_plug": DeviceType.Plug,
|
||||||
|
"is_dimmer": DeviceType.Dimmer,
|
||||||
|
"is_light_strip": DeviceType.LightStrip,
|
||||||
|
"is_wallswitch": DeviceType.WallSwitch,
|
||||||
|
"is_strip": DeviceType.Strip,
|
||||||
|
"is_strip_socket": DeviceType.StripSocket,
|
||||||
|
}
|
||||||
|
deprecated_is_light_function_smart_module = {
|
||||||
|
"is_color": "Color",
|
||||||
|
"is_dimmable": "Brightness",
|
||||||
|
"is_variable_color_temp": "ColorTemperature",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated_attributes(dev: SmartDevice):
|
||||||
|
"""Test deprecated attributes on all devices."""
|
||||||
|
tested_keys = set()
|
||||||
|
|
||||||
|
def _test_attr(attribute):
|
||||||
|
tested_keys.add(attribute)
|
||||||
|
msg = f"{attribute} is deprecated"
|
||||||
|
if module := Device._deprecated_attributes[attribute][0]:
|
||||||
|
msg += f", use: {module} in device.modules instead"
|
||||||
|
with pytest.deprecated_call(match=msg):
|
||||||
|
val = getattr(dev, attribute)
|
||||||
|
return val
|
||||||
|
|
||||||
|
for attribute in deprecated_is_device_type:
|
||||||
|
val = _test_attr(attribute)
|
||||||
|
expected_val = dev.device_type == deprecated_is_device_type[attribute]
|
||||||
|
assert val == expected_val
|
||||||
|
|
||||||
|
for attribute in deprecated_is_light_function_smart_module:
|
||||||
|
val = _test_attr(attribute)
|
||||||
|
if isinstance(dev, SmartDevice):
|
||||||
|
expected_val = (
|
||||||
|
deprecated_is_light_function_smart_module[attribute] in dev.modules
|
||||||
|
)
|
||||||
|
elif hasattr(dev, f"_{attribute}"):
|
||||||
|
expected_val = getattr(dev, f"_{attribute}")
|
||||||
|
else:
|
||||||
|
expected_val = False
|
||||||
|
assert val == expected_val
|
||||||
|
|
||||||
|
assert len(tested_keys) == len(Device._deprecated_attributes)
|
||||||
|
untested_keys = [
|
||||||
|
key for key in Device._deprecated_attributes if key not in tested_keys
|
||||||
|
]
|
||||||
|
assert len(untested_keys) == 0
|
||||||
|
@ -3,10 +3,10 @@ import pytest
|
|||||||
from kasa import DeviceType
|
from kasa import DeviceType
|
||||||
from kasa.iot import IotDimmer
|
from kasa.iot import IotDimmer
|
||||||
|
|
||||||
from .conftest import dimmer, handle_turn_on, turn_on
|
from .conftest import dimmer_iot, handle_turn_on, turn_on
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_set_brightness(dev, turn_on):
|
async def test_set_brightness(dev, turn_on):
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
@ -22,7 +22,7 @@ async def test_set_brightness(dev, turn_on):
|
|||||||
assert dev.is_on == turn_on
|
assert dev.is_on == turn_on
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_set_brightness_transition(dev, turn_on, mocker):
|
async def test_set_brightness_transition(dev, turn_on, mocker):
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
@ -44,7 +44,7 @@ async def test_set_brightness_transition(dev, turn_on, mocker):
|
|||||||
assert dev.brightness == 1
|
assert dev.brightness == 1
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
async def test_set_brightness_invalid(dev):
|
async def test_set_brightness_invalid(dev):
|
||||||
for invalid_brightness in [-1, 101, 0.5]:
|
for invalid_brightness in [-1, 101, 0.5]:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -55,7 +55,7 @@ async def test_set_brightness_invalid(dev):
|
|||||||
await dev.set_brightness(1, transition=invalid_transition)
|
await dev.set_brightness(1, transition=invalid_transition)
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
async def test_turn_on_transition(dev, mocker):
|
async def test_turn_on_transition(dev, mocker):
|
||||||
query_helper = mocker.spy(IotDimmer, "_query_helper")
|
query_helper = mocker.spy(IotDimmer, "_query_helper")
|
||||||
original_brightness = dev.brightness
|
original_brightness = dev.brightness
|
||||||
@ -72,7 +72,7 @@ async def test_turn_on_transition(dev, mocker):
|
|||||||
assert dev.brightness == original_brightness
|
assert dev.brightness == original_brightness
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
async def test_turn_off_transition(dev, mocker):
|
async def test_turn_off_transition(dev, mocker):
|
||||||
await handle_turn_on(dev, True)
|
await handle_turn_on(dev, True)
|
||||||
query_helper = mocker.spy(IotDimmer, "_query_helper")
|
query_helper = mocker.spy(IotDimmer, "_query_helper")
|
||||||
@ -90,7 +90,7 @@ async def test_turn_off_transition(dev, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_set_dimmer_transition(dev, turn_on, mocker):
|
async def test_set_dimmer_transition(dev, turn_on, mocker):
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
@ -108,7 +108,7 @@ async def test_set_dimmer_transition(dev, turn_on, mocker):
|
|||||||
assert dev.brightness == 99
|
assert dev.brightness == 99
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
@turn_on
|
@turn_on
|
||||||
async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
|
async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
|
||||||
await handle_turn_on(dev, turn_on)
|
await handle_turn_on(dev, turn_on)
|
||||||
@ -127,7 +127,7 @@ async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
async def test_set_dimmer_transition_invalid(dev):
|
async def test_set_dimmer_transition_invalid(dev):
|
||||||
for invalid_brightness in [-1, 101, 0.5]:
|
for invalid_brightness in [-1, 101, 0.5]:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -138,6 +138,6 @@ async def test_set_dimmer_transition_invalid(dev):
|
|||||||
await dev.set_dimmer_transition(1, invalid_transition)
|
await dev.set_dimmer_transition(1, invalid_transition)
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
def test_device_type_dimmer(dev):
|
def test_device_type_dimmer(dev):
|
||||||
assert dev.device_type == DeviceType.Dimmer
|
assert dev.device_type == DeviceType.Dimmer
|
||||||
|
@ -26,8 +26,8 @@ from kasa.xortransport import XorEncryption
|
|||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
bulb_iot,
|
bulb_iot,
|
||||||
dimmer,
|
dimmer_iot,
|
||||||
lightstrip,
|
lightstrip_iot,
|
||||||
new_discovery,
|
new_discovery,
|
||||||
plug_iot,
|
plug_iot,
|
||||||
strip_iot,
|
strip_iot,
|
||||||
@ -86,14 +86,14 @@ async def test_type_detection_strip(dev: Device):
|
|||||||
assert d.device_type == DeviceType.Strip
|
assert d.device_type == DeviceType.Strip
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
@dimmer_iot
|
||||||
async def test_type_detection_dimmer(dev: Device):
|
async def test_type_detection_dimmer(dev: Device):
|
||||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||||
assert d.is_dimmer
|
assert d.is_dimmer
|
||||||
assert d.device_type == DeviceType.Dimmer
|
assert d.device_type == DeviceType.Dimmer
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
async def test_type_detection_lightstrip(dev: Device):
|
async def test_type_detection_lightstrip(dev: Device):
|
||||||
d = Discover._get_device_class(dev._last_update)("localhost")
|
d = Discover._get_device_class(dev._last_update)("localhost")
|
||||||
assert d.is_light_strip
|
assert d.is_light_strip
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
@ -180,10 +181,9 @@ async def test_feature_setters(dev: Device, mocker: MockerFixture):
|
|||||||
|
|
||||||
async def _test_features(dev):
|
async def _test_features(dev):
|
||||||
exceptions = []
|
exceptions = []
|
||||||
query = mocker.patch.object(dev.protocol, "query")
|
|
||||||
for feat in dev.features.values():
|
for feat in dev.features.values():
|
||||||
query.reset_mock()
|
|
||||||
try:
|
try:
|
||||||
|
with patch.object(feat.device.protocol, "query") as query:
|
||||||
await _test_feature(feat, query)
|
await _test_feature(feat, query)
|
||||||
# we allow our own exceptions to avoid mocking valid responses
|
# we allow our own exceptions to avoid mocking valid responses
|
||||||
except KasaException:
|
except KasaException:
|
||||||
|
@ -16,7 +16,7 @@ from voluptuous import (
|
|||||||
Schema,
|
Schema,
|
||||||
)
|
)
|
||||||
|
|
||||||
from kasa import KasaException
|
from kasa import KasaException, Module
|
||||||
from kasa.iot import IotDevice
|
from kasa.iot import IotDevice
|
||||||
|
|
||||||
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
|
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
|
||||||
@ -261,27 +261,26 @@ async def test_modules_not_supported(dev: IotDevice):
|
|||||||
|
|
||||||
|
|
||||||
async def test_get_modules():
|
async def test_get_modules():
|
||||||
"""Test get_modules for child and parent modules."""
|
"""Test getting modules for child and parent modules."""
|
||||||
dummy_device = await get_device_for_fixture_protocol(
|
dummy_device = await get_device_for_fixture_protocol(
|
||||||
"HS100(US)_2.0_1.5.6.json", "IOT"
|
"HS100(US)_2.0_1.5.6.json", "IOT"
|
||||||
)
|
)
|
||||||
from kasa.iot.modules import Cloud
|
from kasa.iot.modules import Cloud
|
||||||
from kasa.smart.modules import CloudModule
|
|
||||||
|
|
||||||
# Modules on device
|
# Modules on device
|
||||||
module = dummy_device.get_module("Cloud")
|
module = dummy_device.modules.get("cloud")
|
||||||
assert module
|
assert module
|
||||||
assert module._device == dummy_device
|
assert module._device == dummy_device
|
||||||
assert isinstance(module, Cloud)
|
assert isinstance(module, Cloud)
|
||||||
|
|
||||||
module = dummy_device.get_module(Cloud)
|
module = dummy_device.modules.get(Module.IotCloud)
|
||||||
assert module
|
assert module
|
||||||
assert module._device == dummy_device
|
assert module._device == dummy_device
|
||||||
assert isinstance(module, Cloud)
|
assert isinstance(module, Cloud)
|
||||||
|
|
||||||
# Invalid modules
|
# Invalid modules
|
||||||
module = dummy_device.get_module("DummyModule")
|
module = dummy_device.modules.get("DummyModule")
|
||||||
assert module is None
|
assert module is None
|
||||||
|
|
||||||
module = dummy_device.get_module(CloudModule)
|
module = dummy_device.modules.get(Module.Cloud)
|
||||||
assert module is None
|
assert module is None
|
||||||
|
@ -1,29 +1,28 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from kasa import DeviceType
|
from kasa import DeviceType
|
||||||
from kasa.exceptions import KasaException
|
|
||||||
from kasa.iot import IotLightStrip
|
from kasa.iot import IotLightStrip
|
||||||
|
|
||||||
from .conftest import lightstrip
|
from .conftest import lightstrip_iot
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
async def test_lightstrip_length(dev: IotLightStrip):
|
async def test_lightstrip_length(dev: IotLightStrip):
|
||||||
assert dev.is_light_strip
|
assert dev.is_light_strip
|
||||||
assert dev.device_type == DeviceType.LightStrip
|
assert dev.device_type == DeviceType.LightStrip
|
||||||
assert dev.length == dev.sys_info["length"]
|
assert dev.length == dev.sys_info["length"]
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
async def test_lightstrip_effect(dev: IotLightStrip):
|
async def test_lightstrip_effect(dev: IotLightStrip):
|
||||||
assert isinstance(dev.effect, dict)
|
assert isinstance(dev.effect, dict)
|
||||||
for k in ["brightness", "custom", "enable", "id", "name"]:
|
for k in ["brightness", "custom", "enable", "id", "name"]:
|
||||||
assert k in dev.effect
|
assert k in dev.effect
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
|
async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
|
||||||
with pytest.raises(KasaException):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_effect("Not real")
|
await dev.set_effect("Not real")
|
||||||
|
|
||||||
await dev.set_effect("Candy Cane")
|
await dev.set_effect("Candy Cane")
|
||||||
@ -31,7 +30,7 @@ async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
|
|||||||
assert dev.effect["name"] == "Candy Cane"
|
assert dev.effect["name"] == "Candy Cane"
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
@pytest.mark.parametrize("brightness", [100, 50])
|
@pytest.mark.parametrize("brightness", [100, 50])
|
||||||
async def test_effects_lightstrip_set_effect_brightness(
|
async def test_effects_lightstrip_set_effect_brightness(
|
||||||
dev: IotLightStrip, brightness, mocker
|
dev: IotLightStrip, brightness, mocker
|
||||||
@ -49,7 +48,7 @@ async def test_effects_lightstrip_set_effect_brightness(
|
|||||||
assert payload["brightness"] == brightness
|
assert payload["brightness"] == brightness
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
@pytest.mark.parametrize("transition", [500, 1000])
|
@pytest.mark.parametrize("transition", [500, 1000])
|
||||||
async def test_effects_lightstrip_set_effect_transition(
|
async def test_effects_lightstrip_set_effect_transition(
|
||||||
dev: IotLightStrip, transition, mocker
|
dev: IotLightStrip, transition, mocker
|
||||||
@ -67,12 +66,12 @@ async def test_effects_lightstrip_set_effect_transition(
|
|||||||
assert payload["transition"] == transition
|
assert payload["transition"] == transition
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
async def test_effects_lightstrip_has_effects(dev: IotLightStrip):
|
async def test_effects_lightstrip_has_effects(dev: IotLightStrip):
|
||||||
assert dev.has_effects is True
|
assert dev.has_effects is True
|
||||||
assert dev.effect_list
|
assert dev.effect_list
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
@lightstrip_iot
|
||||||
def test_device_type_lightstrip(dev):
|
def test_device_type_lightstrip(dev):
|
||||||
assert dev.device_type == DeviceType.LightStrip
|
assert dev.device_type == DeviceType.LightStrip
|
||||||
|
@ -9,12 +9,11 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from kasa import KasaException
|
from kasa import KasaException, Module
|
||||||
from kasa.exceptions import SmartErrorCode
|
from kasa.exceptions import SmartErrorCode
|
||||||
from kasa.smart import SmartDevice
|
from kasa.smart import SmartDevice
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
bulb_smart,
|
|
||||||
device_smart,
|
device_smart,
|
||||||
get_device_for_fixture_protocol,
|
get_device_for_fixture_protocol,
|
||||||
)
|
)
|
||||||
@ -123,65 +122,42 @@ async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture):
|
|||||||
|
|
||||||
|
|
||||||
async def test_get_modules():
|
async def test_get_modules():
|
||||||
"""Test get_modules for child and parent modules."""
|
"""Test getting modules for child and parent modules."""
|
||||||
dummy_device = await get_device_for_fixture_protocol(
|
dummy_device = await get_device_for_fixture_protocol(
|
||||||
"KS240(US)_1.0_1.0.5.json", "SMART"
|
"KS240(US)_1.0_1.0.5.json", "SMART"
|
||||||
)
|
)
|
||||||
from kasa.iot.modules import AmbientLight
|
from kasa.smart.modules import Cloud
|
||||||
from kasa.smart.modules import CloudModule, FanModule
|
|
||||||
|
|
||||||
# Modules on device
|
# Modules on device
|
||||||
module = dummy_device.get_module("CloudModule")
|
module = dummy_device.modules.get("Cloud")
|
||||||
assert module
|
assert module
|
||||||
assert module._device == dummy_device
|
assert module._device == dummy_device
|
||||||
assert isinstance(module, CloudModule)
|
assert isinstance(module, Cloud)
|
||||||
|
|
||||||
module = dummy_device.get_module(CloudModule)
|
module = dummy_device.modules.get(Module.Cloud)
|
||||||
assert module
|
assert module
|
||||||
assert module._device == dummy_device
|
assert module._device == dummy_device
|
||||||
assert isinstance(module, CloudModule)
|
assert isinstance(module, Cloud)
|
||||||
|
|
||||||
# Modules on child
|
# Modules on child
|
||||||
module = dummy_device.get_module("FanModule")
|
module = dummy_device.modules.get("Fan")
|
||||||
assert module
|
assert module
|
||||||
assert module._device != dummy_device
|
assert module._device != dummy_device
|
||||||
assert module._device._parent == dummy_device
|
assert module._device._parent == dummy_device
|
||||||
|
|
||||||
module = dummy_device.get_module(FanModule)
|
module = dummy_device.modules.get(Module.Fan)
|
||||||
assert module
|
assert module
|
||||||
assert module._device != dummy_device
|
assert module._device != dummy_device
|
||||||
assert module._device._parent == dummy_device
|
assert module._device._parent == dummy_device
|
||||||
|
|
||||||
# Invalid modules
|
# Invalid modules
|
||||||
module = dummy_device.get_module("DummyModule")
|
module = dummy_device.modules.get("DummyModule")
|
||||||
assert module is None
|
assert module is None
|
||||||
|
|
||||||
module = dummy_device.get_module(AmbientLight)
|
module = dummy_device.modules.get(Module.IotAmbientLight)
|
||||||
assert module is None
|
assert module is None
|
||||||
|
|
||||||
|
|
||||||
@bulb_smart
|
|
||||||
async def test_smartdevice_brightness(dev: SmartDevice):
|
|
||||||
"""Test brightness setter and getter."""
|
|
||||||
assert isinstance(dev, SmartDevice)
|
|
||||||
assert "brightness" in dev._components
|
|
||||||
|
|
||||||
# Test getting the value
|
|
||||||
feature = dev.features["brightness"]
|
|
||||||
assert feature.minimum_value == 1
|
|
||||||
assert feature.maximum_value == 100
|
|
||||||
|
|
||||||
await dev.set_brightness(10)
|
|
||||||
await dev.update()
|
|
||||||
assert dev.brightness == 10
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
await dev.set_brightness(feature.minimum_value - 10)
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
await dev.set_brightness(feature.maximum_value + 10)
|
|
||||||
|
|
||||||
|
|
||||||
@device_smart
|
@device_smart
|
||||||
async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixture):
|
async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixture):
|
||||||
"""Test is_cloud_connected property."""
|
"""Test is_cloud_connected property."""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user