feat(tapo-agent): add schedule/countdown timer API support

- Fork tapo crate to add missing schedule/timer APIs
- Add get_countdown_rules, get_schedule_rules, get_next_event methods
- New readings: countdown_active, countdown_remain, schedule_count,
  schedule_active_count, next_event_time
- Add local compilation to build script alongside cross-compilation
- Implement offline polling - device collection continues when server disconnects
- Add more device readings: on_time, signal_level, rssi, runtime_today/month, energy_month

Vendored tapo fork in tapo-fork/ with minimal changes to add schedule APIs.
This commit is contained in:
seb
2025-12-23 00:46:42 +01:00
parent f3cca149f9
commit 028763bdb2
274 changed files with 20784 additions and 48 deletions

View File

@@ -0,0 +1,5 @@
[advisories]
ignore = [
# The Marvin Attack poses minimal risk to the use cases of this library
"RUSTSEC-2023-0071",
]

View File

@@ -0,0 +1 @@
github: mihai-dinculescu

View File

@@ -0,0 +1,29 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "chore"
prefix-development: "chore"
include: "scope"
- package-ecosystem: pip
directory: "/tapo-py"
schedule:
interval: "daily"
commit-message:
prefix: "chore"
prefix-development: "chore"
include: "scope"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
commit-message:
prefix: "chore"
prefix-development: "chore"
include: "scope"

View File

@@ -0,0 +1,184 @@
# This file is autogenerated by maturin v1.10.2
# To update, run
#
# maturin generate-ci github
#
name: Python
on:
push:
branches:
- main
tags:
- "*"
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-22.04
target: x86_64
- runner: ubuntu-22.04
target: x86
- runner: ubuntu-22.04
target: aarch64
- runner: ubuntu-22.04
target: armv7
- runner: ubuntu-22.04
target: s390x
- runner: ubuntu-22.04
target: ppc64le
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./tapo-py/Cargo.toml
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v5
with:
name: wheels-linux-${{ matrix.platform.target }}
path: dist
musllinux:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: ubuntu-22.04
target: x86_64
- runner: ubuntu-22.04
target: x86
- runner: ubuntu-22.04
target: aarch64
- runner: ubuntu-22.04
target: armv7
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./tapo-py/Cargo.toml
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
manylinux: musllinux_1_2
- name: Upload wheels
uses: actions/upload-artifact@v5
with:
name: wheels-musllinux-${{ matrix.platform.target }}
path: dist
windows:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: windows-latest
target: x64
- runner: windows-latest
target: x86
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
architecture: ${{ matrix.platform.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./tapo-py/Cargo.toml
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Upload wheels
uses: actions/upload-artifact@v5
with:
name: wheels-windows-${{ matrix.platform.target }}
path: dist
macos:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
platform:
- runner: macos-15-intel
target: x86_64
- runner: macos-latest
target: aarch64
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist --find-interpreter --manifest-path ./tapo-py/Cargo.toml
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Upload wheels
uses: actions/upload-artifact@v5
with:
name: wheels-macos-${{ matrix.platform.target }}
path: dist
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist --manifest-path ./tapo-py/Cargo.toml
- name: Test sdist
run: |
pip install --force-reinstall --verbose dist/*.tar.gz
python -c 'from tapo import ApiClient'
- name: Upload sdist
uses: actions/upload-artifact@v5
with:
name: wheels-sdist
path: dist
release:
name: Release
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
needs: [linux, musllinux, windows, macos, sdist]
permissions:
# Use to sign the release artifacts
id-token: write
# Used to upload release artifacts
contents: write
# Used to generate artifact attestation
attestations: write
steps:
- uses: actions/download-artifact@v6
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v3
with:
subject-path: "wheels-*/*"
- name: Publish to PyPI
if: ${{ startsWith(github.ref, 'refs/tags/') }}
uses: PyO3/maturin-action@v1
env:
MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
with:
command: upload
args: --non-interactive --skip-existing wheels-*/*

View File

@@ -0,0 +1,64 @@
name: Rust
on:
push:
branches:
- main
tags:
- "*"
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
checks:
name: Rust checks
runs-on: ubuntu-latest
steps:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: davidB/rust-cargo-make@v1
- uses: actions/checkout@v6
- name: Run format
run: cargo make format
- name: Run check
run: cargo make check
- name: Run check doc
run: cargo make check-doc
- name: Run clippy
run: cargo make clippy
- name: Run test
run: cargo make test
checks_min_ver:
name: Rust checks on the minimum supported version
runs-on: ubuntu-latest
steps:
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.88.0
- uses: davidB/rust-cargo-make@v1
- uses: actions/checkout@v6
- name: Run check on the minimum supported version
run: cargo +1.88.0 make check
publish:
name: Publish
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' }}
needs: [checks, checks_min_ver]
steps:
- uses: actions/checkout@v6
with:
ref: main
- uses: dtolnay/rust-toolchain@stable
- name: Run cargo login
run: cargo login ${CRATES_IO_TOKEN}
env:
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
- name: Run build
run: cargo build --package tapo --release --verbose
- name: Run cargo publish
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
run: cargo publish --package tapo

View File

@@ -0,0 +1,26 @@
name: Security
on:
push:
branches:
- main
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
pull_request:
branches:
- main
paths:
- "**/Cargo.toml"
- "**/Cargo.lock"
schedule:
- cron: "0 0 * * *"
jobs:
security_audit:
name: Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: actions-rs/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}

3
agents/tapo/tapo-fork/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/target
.vscode
/.idea

View File

@@ -0,0 +1,800 @@
# Change Log
All notable changes to this project will be documented in this
file. This change log follows the conventions of
[keepachangelog.com][keepachangelog].
## [Rust Unreleased][Unreleased]
## [Python Unreleased][Unreleased]
## [Rust v0.8.8][v0.8.8] - 2025-11-23
### Added
- `TapoResponseError`: added `Forbidden` variant to represent authentication failures when Third-Party Compatibility is disabled in the Tapo app.
### Changed
- `TapoResponseError`: renamed variant `InvalidCredentials` to `Unauthorized` and updated the variant to include `code` and `description` for improved error context.
## [Python v0.8.8][v0.8.8] - 2025-11-23
### Added
- `Tapo` Exception: added `Forbidden` variant to represent authentication failures when Third-Party Compatibility is disabled in the Tapo app.
### Changed
- `Tapo` Exception: renamed variant `InvalidCredentials` to `Unauthorized` and updated the variant to include `code` and `description` for improved error context.
### Removed
- Dropped support for Python 3.9 and 3.10 (both no longer supported upstream). The minimum required version is now Python 3.11.
## [Rust v0.8.7][v0.8.7] - 2025-11-01
### Added
- `HubHandler`: added the `device_reboot` and `device_reset` methods.
- `PowerStripEnergyMonitoringHandler`: added the `device_reboot` and `device_reset` methods.
- `PowerStripHandler`: added the `device_reboot` and `device_reset` methods.
- `ColorLightHandler`: added the `device_reboot` method.
- `LightHandler`: added the `device_reboot` method.
- `PlugEnergyMonitoringHandler`: added the `device_reboot` method.
- `PlugHandler`: added the `device_reboot` method.
- `RgbicLightStripHandler`: added the `device_reboot` method.
- `RgbLightStripHandler`: added the `device_reboot` method.
### Changed
- `device_reset`: now requires the `DeviceManagementExt` trait to be in scope. The newly added `device_reboot` method also requires this trait.
## [Python v0.8.7][v0.8.7] - 2025-11-01
### Added
- `tapo`: added support for Python 3.14.
- `HubHandler`: added the `device_reboot` and `device_reset` methods.
- `PowerStripEnergyMonitoringHandler`: added the `device_reboot` and `device_reset` methods.
- `PowerStripHandler`: added the `device_reboot` and `device_reset` methods.
- `ColorLightHandler`: added the `device_reboot` method.
- `LightHandler`: added the `device_reboot` method.
- `PlugEnergyMonitoringHandler`: added the `device_reboot` method.
- `PlugHandler`: added the `device_reboot` method.
- `RgbicLightStripHandler`: added the `device_reboot` method.
- `RgbLightStripHandler`: added the `device_reboot` method.
## [Rust v0.8.6][v0.8.6] - 2025-09-25
### Added
- `PlugEnergyMonitoringHandler`: added `get_power_data` method to retrieve historical power data (every 5 minutes & hourly) for energy-monitoring plugs (P110, P110M, P115). The `PowerDataInterval` enum allows specifying the desired interval.
- `PowerStripPlugEnergyMonitoringHandler`: added the following energy monitoring methods: `get_current_power`, `get_device_usage`, `get_energy_usage`, `get_energy_data`, `get_power_data`.
### Changed
- `PlugEnergyMonitoringHandler`: `EnergyDataResult` has been redesigned to provide better ergonomics by attaching a start date time to each interval entry.
### Removed
- `EnergyUsageResult`: the `current_power` field has been removed from the struct because not all energy-monitoring plugs provide this data. Instead, use the `get_current_power` method to retrieve the current power.
## [Python v0.8.6][v0.8.6] - 2025-09-25
### Added
- `PlugEnergyMonitoringHandler`: added `get_power_data` method to retrieve historical power data (every 5 minutes & hourly) for energy-monitoring plugs (P110, P110M, P115). The `PowerDataInterval` enum allows specifying the desired interval.
- `PowerStripPlugEnergyMonitoringHandler`: added the following energy monitoring methods: `get_current_power`, `get_device_usage`, `get_energy_usage`, `get_energy_data`, `get_power_data`.
### Changed
- `PlugEnergyMonitoringHandler`: `EnergyDataResult` has been redesigned to provide better ergonomics by attaching a start date time to each interval entry.
### Removed
- `EnergyUsageResult`: the `current_power` field has been removed from the class because not all energy-monitoring plugs provide this data. Instead, use the `get_current_power` method to retrieve the current power.
## [Rust v0.8.5][v0.8.5] - 2025-09-18
### Added
- `ApiClient`: added `discover_devices` method to discover all Tapo devices on the local network. This works even with dynamic or unknown IPs, but is slower since it scans the entire network and waits for device responses.
- `PowerStripPlugHandler`: added support for P306 power strips.
- Added `PowerStripEnergyMonitoringHandler`, `PowerStripPlugEnergyMonitoringHandler`, and `PowerStripPlugEnergyMonitoringResult` to support energy-monitoring power strips (P304M, P316M). Non-monitoring models (P300, P306) will continue using the pre-existing `PowerStripPlugHandler`.
- `PowerStripPlugResult`: added `default_states` field.
### Changed
- `DeviceInfoPowerStripResult`: changed `time_diff` from `Option<i64>` to `i64`.
### Fixed
- `PowerStripPlugResult`: removed `charging_status`, `overcurrent_status`, and `power_protection_status` (not returned by P300/P306).
## [Python v0.8.5][v0.8.5] - 2025-09-18
### Added
- `ApiClient`: added `discover_devices` method to discover all Tapo devices on the local network. This works even with dynamic or unknown IPs, but is slower since it scans the entire network and waits for device responses.
- `PowerStripPlugHandler`: added support for P306 power strips.
- Added `PowerStripEnergyMonitoringHandler`, `PowerStripPlugEnergyMonitoringHandler`, and `PowerStripPlugEnergyMonitoringResult` to support energy-monitoring power strips (P304M, P316M). Non-monitoring models (P300, P306) will continue using the pre-existing `PowerStripPlugHandler`.
- `PowerStripPlugResult`: added `default_states` field.
### Changed
- `DeviceInfoPowerStripResult`: changed `time_diff` from `Optional[int]` to `int`.
### Fixed
- `PowerStripPlugResult`: removed `charging_status`, `overcurrent_status`, and `power_protection_status` (not returned by P300/P306).
## [Rust v0.8.4][v0.8.4] - 2025-08-11
### Added
- The `default_states` property has been added to the `DeviceInfoPlugResult` struct to provide a more comprehensive overview of the plug's state.
### Fixed
- The `default_states` property value of the `DeviceInfoPlugEnergyMonitoringResult` struct has been changed from a struct to an enum to better reflect the actual response from the device.
## [Python v0.8.4][v0.8.4] - 2025-08-11
### Added
- The `default_states` property has been added to the `DeviceInfoPlugResult` class to provide a more comprehensive overview of the plug's state.
### Fixed
- The `default_states` property value of the `DeviceInfoPlugEnergyMonitoringResult` class has been changed from a class to an enum to better reflect the actual response from the device.
## [Rust v0.8.3][v0.8.3] - 2025-07-25
### Added
- Added support for the P316M power strips.
- The `charging_status`, `overcurrent_status`, and `power_protection_status` fields have been added to `PowerStripPlugResult`.
### Changed
- Enhanced the `InvalidCredentials` error with clearer guidance on common causes and how to address them.
- The `overheat_status` field in `PowerStripPlugResult` is now optional to support devices that omit this field.
### Removed
- Removed `nickname` from `DeviceInfoPowerStripResult` because it is not present in the response.
## [Python v0.8.3][v0.8.3] - 2025-07-25
### Added
- Added support for the P316M power strips.
- The `charging_status`, `overcurrent_status`, and `power_protection_status` fields have been added to `PowerStripPlugResult`.
### Changed
- Enhanced the `InvalidCredentials` error with clearer guidance on common causes and how to address them.
- The `overheat_status` field in `PowerStripPlugResult` is now optional to support devices that omit this field.
### Removed
- Removed `nickname` from `DeviceInfoPowerStripResult` because it is not present in the response.
## [Rust v0.8.2][v0.8.2] - 2025-05-19
### Added
- The `charging_status` field has been added to `DeviceInfoPlugEnergyMonitoringResult`.
### Changed
- The `overheat_status` field in `DeviceInfoPlugEnergyMonitoringResult` is now optional to support devices that omit this field after the latest firmware update (1.3.4 Build 250403 Rel.150504).
## [Python v0.8.2][v0.8.2] - 2025-05-19
### Added
- The `charging_status` field has been added to `DeviceInfoPlugEnergyMonitoringResult`.
### Changed
- The `overheat_status` field in `DeviceInfoPlugEnergyMonitoringResult` is now optional to support devices that omit this field after the latest firmware update (1.3.4 Build 250403 Rel.150504).
## [Rust v0.8.1][v0.8.1] - 2025-02-10
### Added
- Added functionality for controlling the alarm on the H100 hub via the `play_alarm` and `stop_alarm` methods in the `H100Handler`. Additionally, `get_supported_ringtone_list` is available to retrieve the list of supported ringtones for debugging purposes. (thanks to @kay)
- Added the ability to retrieve the color configuration (`hue`, `saturation`, `color_temperature`) for the `Color` enum values through the `get_color_config` method. (thanks to @WhySoBad)
### Changed
- The internal implementation of `H100Handler`'s `get_child_device_list` has been updated to fetch all pages, not just the first one.
- `H100Handler`'s `get_child_device_list_json` now includes a `start_index` parameter to fetch child devices starting from a specific index.
### Fixed
- Resolved an issue that caused the passthrough protocol test to incorrectly indicate support when it was not actually supported. (thanks to @WhySoBad)
## [Python v0.8.1][v0.8.1] - 2025-02-10
### Added
- Added functionality for controlling the alarm on the H100 hub via the `play_alarm` and `stop_alarm` methods in the `H100Handler`. Additionally, `get_supported_ringtone_list` is available to retrieve the list of supported ringtones for debugging purposes.
- Added the ability to retrieve the color configuration (`hue`, `saturation`, `color_temperature`) for the `Color` enum values through the `get_color_config` method. (thanks to @WhySoBad)
### Changed
- The internal implementation of `H100Handler`'s `get_child_device_list` has been updated to fetch all pages, not just the first one.
- `H100Handler`'s `get_child_device_list_json` now includes a `start_index` parameter to fetch child devices starting from a specific index.
### Fixed
- Resolved an issue that caused the passthrough protocol test to incorrectly indicate support when it was not actually supported. (thanks to @WhySoBad)
## [Rust v0.8.0][v0.8.0] - 2024-12-07
This marks the first unified release of the Rust and Python libraries. Moving forward, both libraries will be released simultaneously and will share the same version number.
### Added
- Added an example for the L900 light strips.
### Changed
- `LightingEffect`'s `fadeoff` field has been renamed to `fade_off`, and its `with_fadeoff` method has been renamed to `with_fade_off`.
- `LightingEffect`'s `new_with_random_id` function has been removed. The `new` function now creates a `LightingEffect` instance with a random ID by default.
## [Python v0.8.0][v0.8.0] - 2024-12-07
### Added
- Added support for the L900 light strips.
- Added support for the L920 and L930 light strips.
- Added support for Python 3.13.
## [Python v0.7.0][py-v0.7.0] - 2024-11-07
### Added
- Added support for the KE100 thermostatic radiator valve (TRV).
## [Rust v0.7.17][v0.7.17] - 2024-10-23
### Added
- Added support for the P304 power strip.
### Changed
- The `openssl` dependency has been replaced with native Rust alternatives to expand cross-compilation options, such as for Android, and to decrease build times (thanks to @rbock44).
- `PlugPowerStripHandler` has been renamed to `PowerStripPlugHandler` to be consistent with the rest of the library.
- `PlugPowerStripResult` has been renamed to `PowerStripPlugResult` to be consistent with the rest of the library.
- The `UsageByPeriodResult` fields `today`, `past7`, and `past30` have been updated to `Option<u64>` to handle cases where the API returns negative values, which will be represented as `None`.
### Fixed
- Updated all comments referencing Watts to confirm the correct units are specified.
## [Python v0.6.0][py-v0.6.0] - 2024-10-23
### Added
- Added support for the P300 and P304 power strips.
- Python logs can now capture entries from the underlying Rust library.
### Changed
- The `openssl` dependency has been replaced with native Rust alternatives to expand cross-compilation options, such as for Android, and to decrease build times (thanks to @rbock44).
- The `UsageByPeriodResult` fields `today`, `past7`, and `past30` have been updated to `Optional[int]` to handle cases where the API returns negative values, which will be represented as `null`.
### Fixed
- Updated all comments referencing Watts to confirm the correct units are specified.
## [Rust v0.7.16][v0.7.16] - 2024-09-27
### Added
- Added support for the L535 light bulbs.
### Fixed
- Fixed an issue that prevented the color from being set properly for the L535 light bulbs.
## [Python v0.5.1][py-v0.5.1] - 2024-09-27
### Added
- Added support for the L535 light bulbs.
### Fixed
- Fixed an issue that prevented the color from being set properly for the L535 light bulbs.
## [Rust v0.7.15][v0.7.15] - 2024-09-18
### Added
- The `LowBattery` variant has been added to the `S200BLog` enum.
### Changed
- The `t310` and `t315` methods of `HubHandler` can now create `T31XHandler` handlers for either of the two device types.
- The child device handlers for the H100 hub and the P300 power strip have been redesigned to eliminate the use of lifetimes, to facilitate FFI integrations.
- The comments of `start_timestamp` and `end_timestamp` fields in `EnergyDataResult` have been updated to better describe their purpose.
- `S200BRotationParams`'s field `degrees` has been renamed to `rotation_degrees`.
### Fixed
- Fixed an issue with the `Color` presets that triggered a validation error when attempting to set the `color` to `DarkRed`.
### Removed
- The deprecated `past24h`, `past7d`, `past30d` and `past1y` fields have been removed from `EnergyUsageResult`. This data is now available exclusively through `get_energy_data`'s `EnergyDataResult` response.
## [Python v0.5.0][py-v0.5.0] - 2024-09-18
### Added
- Added full support for the S200B switches through the `S200BHandler` handler.
- Added full support for the T100 sensors through the `T100Handler` handler.
- Added full support for the T110 sensors through the `T110Handler` handler.
- Added full support for the T300 sensors through the `T300Handler` handler.
- Added full support for the T310 and T315 sensors through the `T31XHandler` handler.
### Changed
- The comments of `start_timestamp` and `end_timestamp` fields in `EnergyDataResult` have been updated to better describe their purpose.
### Fixed
- Fixed an issue with the `Color` presets that triggered a validation error when attempting to set the `color` to `DarkRed`.
## [Rust v0.7.14][v0.7.14] - 2024-08-31
### Changed
- `DeviceInfoPlugEnergyMonitoringResult` has been added to support the P110 and P115 devices, which have different responses compared to the P100 and P105 devices.
### Fixed
- `DeviceInfoPlugResult` has been updated to correctly support the P100 and P105 devices.
## [Python v0.4.0][py-v0.4.0] - 2024-08-31
### Changed
- `DeviceInfoPlugEnergyMonitoringResult` has been added to support the P110 and P115 devices, which have different responses compared to the P100 and P105 devices.
### Fixed
- Resolved an issue that led to unrecoverable process hangs when a device request timed out.
- The concurrency of device handlers has been significantly enhanced by replacing all `Mutex` instances with `RwLock`.
- `DeviceInfoPlugResult` has been updated to correctly support the P100 and P105 devices.
## [Rust v0.7.13][v0.7.13] - 2024-08-26
### Changed
- To align with the latest API updates, the `overheated` field for plugs has been replaced by three enums: `overcurrent_status`, `overheat_status`, and `power_protection_status` (thanks to @padenot).
## [Python v0.3.2][py-v0.3.2] - 2024-08-26
### Changed
- To align with the latest API updates, the `overheated` field for plugs has been replaced by three enums: `overcurrent_status`, `overheat_status`, and `power_protection_status`.
## [Rust v0.7.12][v0.7.12] - 2024-06-27
### Changed
- H100's create child device handler methods now take a `HubDevice` `enum` instead of a `String` and are now `async` to allow for more flexibility. This enables the caller to find child devices by either device ID or nickname.
- `PlugIdentifier` has been renamed to `Plug`.
- `Plug::ByDeviceId` now verifies that the provided device ID is found and returns an `Error::DeviceNotFound` error when it's not.
- `HubDevice` variants now take a `String` instead of a `&str` to allow for more flexibility.
- `Plug` variants now take a `String` instead of a `&str` to allow for more flexibility.
### Fixed
- `ColorLightSetDeviceInfoParams` `hue` field validation has been changed from `between 1 and 360` to `between 0 and 360` to match the device's expected range.
- Fixed an issue where the `EnergyDataResult`'s `start_timestamp` and `end_timestamp` did not correctly adjust for timezone offsets.
- The `chrono` dependency has been updated to `0.4.34` to fix the minimum version requirement.
### Removed
- The `overheated` property has been removed from `DeviceInfoGenericResult` because it's not present in the response of all devices.
## [Python v0.3.1][py-v0.3.1] - 2024-06-27
### Fixed
- `ColorLightSetDeviceInfoParams` `hue` field validation has been changed from `between 1 and 360` to `between 0 and 360` to match the device's expected range.
- Fixed an issue where the `EnergyDataResult`'s `start_timestamp` and `end_timestamp` did not correctly adjust for timezone offsets.
- All handlers are now correctly exported and can be imported from the `tapo` module.
### Removed
- The `overheated` property has been removed from `DeviceInfoGenericResult` because it's not present in the response of all devices.
## [Rust v0.7.11][v0.7.11] - 2024-05-04
### Added
- Added support for the P300 power strip (thanks to @Michal-Szczepaniak).
- `RgbLightStripHandler` and `DeviceInfoRgbLightStripResult` have been added to support the L900 devices separately from the L530 and L630 devices.
### Changed
- `ChildDeviceResult` has been renamed to `ChildDeviceHubResult` to facilitate adding support for other devices with children.
- `ColorLightStripHandler` has been renamed to `RgbicLightStripHandler` to better reflect its purpose.
- `DeviceInfoColorLightStripResult` has been renamed to `DeviceInfoRgbicLightStripResult` to better reflect its purpose.
## [Python v0.3.0][py-v0.3.0] - 2024-05-04
### Added
- Added partial support for the H100 hub and its child devices. Currently, only the `get_device_info` function is supported for the child devices through the hub's `get_child_device_list` method.
### Changed
- A large number of types have been reorganized to me more in line with the Rust library. This includes moving many of them under the `requests` and `responses` sub modules.
### Removed
- `l900` has been removed from the `ApiClient` until proper support is added.
## [Rust v0.7.10][v0.7.10] - 2024-04-05
### Changed
- The implementation of `ApiClient::new` has been improved to allow for the return of `ApiClient` instead of `Result<ApiClient, Error>`.
- The default timeout for all requests has been reduced to 30 seconds from 300 seconds.
- `ApiClient::with_timeout` has been added to allow for the setting of a custom timeout for all requests (thanks to @skoky).
## [Python v0.2.1][py-v0.2.1] - 2024-04-05
### Changed
- The default timeout for all requests has been reduced to 30 seconds from 300 seconds.
- The `timeout_s` optional parameter has been added to the `ApiClient` constructor to allow for the setting of a custom timeout for all requests (thanks to @skoky).
## [Rust v0.7.9][v0.7.9] - 2024-01-27
### Changed
- The `send()` method of the `.set()` API now takes a reference to the device handler in order to allow for better ergonomics.
### Fixed
- The device info response for the L510, L520, and L610 devices has been fixed.
## [Python v0.2.0][py-v0.2.0] - 2024-01-27
### Added
- Added support for the L530, L630, and L900 color light bulbs.
### Fixed
- Fixed a misconfiguration that was preventing the sdist package from working properly.
- The device info response for the L510, L520, and L610 devices has been fixed.
## [Rust v0.7.8][v0.7.8] - 2024-01-22
### Added
- Added the `device_reset` method to all plugs and lights.
### Fixed
- The device info response for the L510, L520, and L610 devices has been fixed to have the `re_power_type` field as optional.
## [Python v0.1.5][py-v0.1.5] - 2024-01-22
### Added
- Added the `device_reset` method to all plugs and lights.
### Fixed
- The device info response for the L510, L520, and L610 devices has been fixed to have the `re_power_type` field as optional.
## [Rust v0.7.7][v0.7.7] - 2024-01-13
### Changed
- The `anyhow::anyhow!("Local hash does not match server hash")` error has been replaced with the more specific `tapo::TapoResponseError::InvalidCredentials` error.
### Fixed
- The `default_states` field that's part of the device info response has been changed for the L510, L520, and L610 devices to match the actual response from the device.
- A handful of edge cases around the Klap Protocol that were causing panics have been fixed to return `tapo::TapoResponseError::SessionTimeout` or `tapo::TapoResponseError::InvalidResponse` errors instead.
## [Python v0.1.4][py-v0.1.4] - 2024-01-13
### Changed
- The "Local hash does not match server hash" error has been replaced with the more specific `tapo::TapoResponseError::InvalidCredentials` error.
### Fixed
- The `default_states` field that's part of the device info response has been changed for the L510, L520, and L610 devices to match the actual response from the device.
- A handful of edge cases around the Klap Protocol that were causing panics have been fixed to return `tapo::TapoResponseError::SessionTimeout` or `tapo::TapoResponseError::InvalidResponse` errors instead.
## [Rust v0.7.6][v0.7.6] - 2023-11-25
### Added
- Added support for the KE100 thermostatic radiator valve (TRV) devices (thanks to @pwoerndle).
### Fixed
- Fixed an issue that was preventing the `nickname` field from being decoded in the `get_device_info` results of child devices of the H100 hub.
## [Rust v0.7.5][v0.7.5] - 2023-11-05
### Added
- Added support for the T300 water sensor.
- Added a dedicated handler for the L520 devices.
## [Python v0.1.3][py-v0.1.3] - 2023-11-04
### Added
- Added support for the L510, L520 and L610 light bulbs.
### Changed
- The minimum required version of Python has been changed to 3.8, up from 3.7.
### Fixed
- Fixed an issue that was preventing `get_device_info_json` from working on the plug devices.
## [Python v0.1.2][py-v0.1.2] - 2023-10-19
### Added
- Added support for generic devices.
- Added `get_device_info_json` to all currently supported devices.
## [Python v0.1.1][py-v0.1.1] - 2023-10-01
This is the first version of the Python wrapper library.
It supports the plug devices P100, P105, P110 and P115.
## [v0.7.4] - 2023-09-15
### Fixed
- Fixed the minimum version of the chrono dependency by setting it to 0.4.25.
### Changes
- `DeviceUsageResult` has been split into `DeviceUsageResult` and `DeviceUsageEnergyMonitoringResult`. The former is now returned for the P100 and P105 devices while the latter is returned for all the other devices that support energy monitoring.
- `EnergyMonitoringPlugHandler` has been renamed to `PlugEnergyMonitoringHandler`.
- All `___DeviceInfoResult` structs have been renamed to `DeviceInfo___Result`.
- All `___DefaultState` structs have been renamed to `Default___State`.
### Removed
- `get_device_usage` has been removed from the `GenericDeviceHandler` because it is not supported by all devices.
## [v0.7.3] - 2023-09-14
### Added
- Added support for the newly introduced KLAP protocol, which is required to interact with the latest firmware version of multiple devices.
### Changed
- All uses of `time` have been replaced with `chrono`:
- `EnergyDataInterval`'s `time::OffsetDateTime` and `time::Date` fields have been replaced with `chrono::NaiveDate`.
- `EnergyUsageResult::local_time` field is now `chrono::NaiveDateTime` instead of `time::OffsetDateTime`.
- `EnergyDataResult::local_time` field is now `chrono::NaiveDateTime` instead of `time::OffsetDateTime`.
- `TemperatureHumidityRecords`'s and `TemperatureHumidityRecord` `datetime` fields are now `chrono::DateTime<chrono::Utc>` instead of `time::OffsetDateTime`.
- `EnergyDataInterval::Hourly::start_datetime` and `EnergyDataInterval::Hourly::end_datetime` have been renamed to `start_date` and `end_date` because the time component is not required.
- The `login` function on all handlers has been renamed to `refresh_session` to better reflect its purpose and it now takes and returns a `&mut self` instead of `self`.
- `L510DeviceInfoResult` has been renamed to `LightDeviceInfoResult` to better reflect its purpose when used for L510 and L610 devices.
- `L530DeviceInfoResult` has been renamed to `ColorLightDeviceInfoResult` to better reflect its purpose when used for L530, L630 and L900 devices.
- `L930DeviceInfoResult` has been renamed to `ColorLightStripDeviceInfoResult` to better reflect its purpose when used for L920 and L930 devices.
- The `default_states` field of `LightDeviceInfoResult`, `ColorLightDeviceInfoResult`, `ColorLightStripDeviceInfoResult` and `PlugDeviceInfoResult` is now a struct instead of an enum.
## [v0.7.2] - 2023-08-21
### Added
- Added `get_current_power` to the `P110` and `P115` plugs. (thanks to @Michal-Szczepaniak)
## [v0.7.1] - 2023-05-30
### Added
- Added `get_temperature_humidity_records` to the `T310` and `T315` sensors.
### Changed
- The creation of device handlers has been simplified.
```rust
// old
let device = ApiClient::new(ip_address, tapo_username, tapo_password)?
.l530()
.login()
.await?;
// new
let device = ApiClient::new(tapo_username, tapo_password)?
.l530(ip_address)
.await?;
```
- The creation of child device handlers has been reworked so that they can be created without requiring a call to `get_child_device_list` when the child Device ID is known.
- `ApiClient` now implements `Clone` to allow for a cheaper duplication of the client.
### Removed
- The `L510` and `L610` devices no longer expose the `set()` API because changing multiple properties simultaneously does not make sense for these devices.
## [v0.7.0] - 2023-05-26
### Added
- Added initial support for the H100 device, the S200B switch and the T100, T110, T310, T315 sensors. The child devices currently support `get_device_info` and `get_trigger_logs`.
- All responses now derive `serde::Serialize` to allow for more straightforward consumer serialisation. (thanks to @ClementNerma)
- `ApiClient` has been marked as both `Send` and `Sync` to allow for sharing between threads. (thanks to @ClementNerma)
### Changed
- `GenericDeviceInfoResult`'s `device_on` property has been made optional to accommodate devices that do not provide this field.
## [v0.6.0] - 2023-05-08
### Added
- Added support for the L920 and L930 light strips. The highlight is the `tapo::ColorLightStripHandler::set_lighting_effect` function, which supports all the effects that the Tapo app contains alongside user-defined effects.
- Added support for the L900 light strips.
- Each supported device now has it's own handler creator.
### Changed
- `set_*` functions like `tapo::requests::ColorLightSetDeviceInfoParams::set_brightness` now return `Self` instead of `Result<Self, Error>` to allow for better ergonomics. The validations will now run when `tapo::requests::ColorLightSetDeviceInfoParams::send` is called.
- `tapo::requests::L510SetDeviceInfoParams` has been renamed to `tapo::requests::LightSetDeviceInfoParams` to better reflect its purpose when used for L510, L610, and L900 devices.
- `tapo::requests::L530SetDeviceInfoParams` has been renamed to `tapo::requests::ColorLightSetDeviceInfoParams` to better reflect its purpose when used for L530, L630, L920 and L930 devices.
- `tapo::P100Handler` has been renamed to `tapo::PlugHandler`.
- `tapo::P110Handler` has been renamed to `tapo::EnergyMonitoringPlugHandler`.
- `tapo::L510Handler` has been renamed to `tapo::LightHandler`.
- `tapo::L530Handler` has been renamed to `tapo::ColorLightHandler`.
- `tapo::L930Handler` has been renamed to `tapo::ColorLightStripHandler`.
## [v0.5.0] - 2023-04-16
### Changed
- The creation of an API Client for a specific device is now done through handler methods on the `ApiClient` struct. This allows for a more ergonomic API. (thanks to [Octocrab](https://github.com/XAMPPRocky/octocrab) for inspirations)
```rust
// old
let device = ApiClient::<L530>::new(ip_address, tapo_username, tapo_password, true).await?;
// new
let device = ApiClient::new(ip_address, tapo_username, tapo_password)?
.l530()
.login()
.await?;
```
- `ApiClient::new` parameters are now `impl Into<String>` instead of `String` to allow for more flexibility.
- Error handling has been reworked. All functions that could error now return a `Result<..., tapo::Error>`.
## [v0.4.0] - 2023-02-25
### Added
- `get_energy_data` is now available for the *P110* devices. (thanks to @kuhschnappel)
### Changed
- `EnergyUsageResult`'s `past24h`, `past7d`, `past30d` and `past1y` fields are now deprecated. `get_energy_data` should be used instead. (thanks to @felixhauptmann)
## [v0.3.1] - 2023-02-19
### Added
- `examples/tapo_generic_device_toggle.rs` demonstrates how `device_info` can be used to assess the current status of a generic device and toggle it.
### Changed
- `on_time` is now optional for the `L510` and `L530` devices because the v2 hardware no longer returns it.
## [v0.3.0] - 2022-11-20
### Added
- The `set` API allows multiple properties to be set in a single request for the _L510_ and _L530_ devices.
### Changed
- `tapo::Color` has been moved to `tapo::requests::Color`.
- `GenericDeviceInfoResult::on_time` has been changed from `u64` to `Option<u64>` because some devices (like *L930*) do not provide this field.
- All response structs have been moved under `tapo::responses`.
- The docs have been improved.
## [v0.2.1] - 2022-08-07
### Changed
- `latitude` and `longitude` in `GenericDeviceInfoResult`, `L510DeviceInfoResult`, `L530DeviceInfoResult` and `PlugDeviceInfoResult` are now signed integers to accommodate for incoming responses with negative numbers. (thanks to @JPablomr)
## [v0.2.0] - 2022-06-13
### Added
- Generic Device example.
### Changed
- `get_device_usage` has been moved to the base implementation so that all devices have access to it.
- `Color` now implements `serde::Serialize` and `serde::Deserialize`.
### Removed
- `TapoDeviceExt` is no longer has `Default` and `serde::Serialize` as supersets.
## [v0.1.0] - 2022-06-07
### Initial Release of Tapo
[Unreleased]: https://github.com/mihai-dinculescu/tapo
[v0.8.7]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.7
[v0.8.6]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.6
[v0.8.5]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.5
[v0.8.4]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.4
[v0.8.3]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.3
[v0.8.2]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.2
[v0.8.1]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.1
[v0.8.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.8.0
[py-v0.7.0]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.7.0
[v0.7.17]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.17
[py-v0.6.0]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.6.0
[v0.7.16]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.16
[py-v0.5.1]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.5.1
[v0.7.15]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.15
[py-v0.5.0]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.5.0
[v0.7.14]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.14
[py-v0.4.0]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.4.0
[v0.7.13]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.13
[py-v0.3.2]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.3.2
[v0.7.12]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.12
[py-v0.3.1]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.3.1
[v0.7.11]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.11
[py-v0.3.0]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.3.0
[v0.7.10]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.10
[py-v0.2.1]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.2.1
[v0.7.9]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.9
[py-v0.2.0]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.2.0
[v0.7.8]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.8
[py-v0.1.5]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.1.5
[v0.7.7]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.7
[py-v0.1.4]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.1.4
[v0.7.6]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.6
[v0.7.5]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.5
[py-v0.1.3]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.1.3
[py-v0.1.2]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.1.2
[py-v0.1.1]: https://github.com/mihai-dinculescu/tapo/tree/py-v0.1.1
[v0.7.4]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.4
[v0.7.3]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.3
[v0.7.2]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.2
[v0.7.1]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.1
[v0.7.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.7.0
[v0.6.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.6.0
[v0.5.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.5.0
[v0.4.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.4.0
[v0.3.1]: https://github.com/mihai-dinculescu/tapo/tree/v0.3.1
[v0.3.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.3.0
[v0.2.1]: https://github.com/mihai-dinculescu/tapo/tree/v0.2.1
[v0.2.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.2.0
[v0.1.0]: https://github.com/mihai-dinculescu/tapo/tree/v0.1.0
[keepachangelog]: https://keepachangelog.com

View File

@@ -0,0 +1,28 @@
# Contributing
Contributions are welcome and encouraged! See [/issues][issues] for ideas, or suggest your own!
If you're thinking to create a PR with large feature/change, please first discuss it in an issue.
[issues]: https://github.com/mihai-dinculescu/tapo/issues
## Releasing new versions
- Update version in `tapo/Cargo.toml`
- Update version in `tapo-py/pyproject.toml` (two places)
- Update CHANGELOG.md
- Commit
- Add tag
```bash
git tag -a vX.X.X -m "vX.X.X"
```
- Push
```bash
git push --follow-tags
```
- Create the [release][release].
[releases]: https://github.com/mihai-dinculescu/tapo/releases

View File

@@ -0,0 +1,13 @@
[workspace]
resolver = "3"
members = ["tapo", "tapo-py"]
[workspace.dependencies]
anyhow = "1.0"
chrono = { version = "0.4.34", default-features = false }
log = "0.4"
pyo3 = { version = "0.27" }
serde = { version = "1.0" }
serde_json = { version = "1.0" }
tokio = { version = "1.48", default-features = false }

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022-2025 Mihai Dinculescu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,29 @@
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
[config]
skip_core_tasks = true
[tasks.format]
command = "cargo"
args = ["fmt", "--verbose", "--", "--check"]
[tasks.check]
command = "cargo"
args = ["check", "--verbose"]
[tasks.check-doc]
env = { "RUSTDOCFLAGS" = "-D warnings" }
command = "cargo"
args = ["doc", "--no-deps"]
[tasks.clippy]
command = "cargo"
args = ["clippy", "--all-targets", "--all-features", "--verbose", "--", "-D", "warnings"]
[tasks.test]
command = "cargo"
args = ["test", "--verbose"]
[tasks.ci-flow]
dependencies = ["format", "check", "check-doc", "clippy", "test"]

View File

@@ -0,0 +1,111 @@
# Tapo
[![License][license_badge]][license]
[![Crates][crates_badge]][crates]
[![Documentation][crates_documentation_badge]][crates_documentation]
[![Crates.io][crates_downloads_badge]][crates]
[![PyPI][pypi_badge]][pypi]
[![Python][pypi_versions_badge]][pypi]
[![PyPI][pypi_downloads_badge]][pypi]\
Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L535, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P110M, P115), power strips (P300, P304M, P306, P316M), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315).
[license_badge]: https://img.shields.io/crates/l/tapo.svg
[license]: https://github.com/mihai-dinculescu/tapo/blob/main/LICENSE
[crates_badge]: https://img.shields.io/crates/v/tapo.svg?logo=rust&color=F75101
[crates]: https://crates.io/crates/tapo
[crates_documentation_badge]: https://img.shields.io/docsrs/tapo.svg?logo=rust&color=F75101
[crates_documentation]: https://docs.rs/tapo
[crates_downloads_badge]: https://img.shields.io/crates/d/tapo?logo=rust&label=downloads&color=F75101
[pypi_badge]: https://img.shields.io/pypi/v/tapo.svg?logo=pypi&color=00ADD4
[pypi]: https://pypi.org/project/tapo
[pypi_versions_badge]: https://img.shields.io/pypi/pyversions/tapo.svg?logo=python&color=00ADD4
[pypi_downloads_badge]: https://img.shields.io/pypi/dm/tapo?logo=python&color=00ADD4
## Supported Devices
See [/SUPPORTED_DEVICES.md][supported_devices] for the supported devices and feature matrix.
## Rust
### Usage
> Cargo.toml
```toml
[dependencies]
tapo = "0.8"
```
> main.rs
```rust
let device = ApiClient::new("<tapo-username>", "tapo-password")
.p110("<device ip address>")
.await?;
device.on().await?;
```
### Examples
```bash
export TAPO_USERNAME=
export TAPO_PASSWORD=
export IP_ADDRESS=
cargo run --example tapo_l530
```
See all examples in [/tapo/examples][examples].
### Wrapper REST API
[tapo-rest][tapo_rest] is a REST wrapper of this library that can be deployed as a service or serve as an advanced example.
## Python
### Usage
```bash
pip install tapo
```
```python
client = ApiClient("<tapo-username>", "tapo-password")
device = await client.p110("<device ip address>")
await device.on()
```
### Examples
```bash
cd tapo-py
poetry install # On the initial run
poetry shell
maturin develop # On the initial run and whenever the Rust code is modified
export TAPO_USERNAME=
export TAPO_PASSWORD=
export IP_ADDRESS=
```
```bash
python examples/tapo_p110.py
```
See all examples in [/tapo-py/examples][examples-py].
## Contributing
Contributions are welcome and encouraged! See [/CONTRIBUTING.md][contributing].
## Credits
Inspired by [petretiandrea/plugp100][inspired_by].
[supported_devices]: https://github.com/mihai-dinculescu/tapo/blob/main/SUPPORTED_DEVICES.md
[examples]: https://github.com/mihai-dinculescu/tapo/tree/main/tapo/examples
[examples-py]: https://github.com/mihai-dinculescu/tapo/tree/main/tapo-py/examples
[tapo_rest]: https://github.com/ClementNerma/tapo-rest
[contributing]: https://github.com/mihai-dinculescu/tapo/blob/main/CONTRIBUTING.md
[inspired_by]: https://github.com/petretiandrea/plugp100

View File

@@ -0,0 +1,74 @@
# Supported devices
&check; - Rust only\
&#x2705; - Rust and Python
| Feature<br/><br/><br/> | GenericDevice<br/><br/><br/> | L510<br/>L520<br/>L610<br/> | L530<br/>L535<br/>L630<br/> | L900<br/><br/><br/> | L920<br/>L930<br/><br/> | P100<br/>P105<br/><br/> | P110<br/>P110M<br/>P115<br/> | P300<br/>P306<br/><br/> | P304M<br/>P316M<br/><br/> | H100<br/><br/><br/> |
| ------------------------------------ | :--------------------------- | :-------------------------- | :-------------------------- | :------------------ | :---------------------- | :---------------------- | :--------------------------- | :---------------------- | :------------------------ | :------------------ |
| device_reboot | | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| device_reset | | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| get_child_device_component_list_json | | | | | | | | &#x2705; | &#x2705; | &#x2705; |
| get_child_device_list | | | | | | | | &#x2705; | &#x2705; | &#x2705; |
| get_child_device_list_json | | | | | | | | &#x2705; | &#x2705; | &#x2705; |
| get_current_power | | | | | | | &#x2705; | | | |
| get_device_info | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| get_device_info_json | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| get_device_usage | | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | | | |
| get_energy_data | | | | | | | &#x2705; | | | |
| get_energy_usage | | | | | | | &#x2705; | | | |
| get_power_data | | | | | | | &#x2705; | | | |
| get_supported_ringtone_list | | | | | | | | | | &#x2705; |
| off | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | | | |
| on | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | | | |
| play_alarm | | | | | | | | | | &#x2705; |
| refresh_session | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| set_brightness | | &#x2705; | &#x2705; | &#x2705; | &#x2705; | | | | | |
| set_color | | | &#x2705; | &#x2705; | &#x2705; | | | | | |
| set_color_temperature | | | &#x2705; | &#x2705; | &#x2705; | | | | | |
| set_hue_saturation | | | &#x2705; | &#x2705; | &#x2705; | | | | | |
| set_lighting_effect | | | | | &#x2705; | | | | | |
| set() API \* | | | &#x2705; | &#x2705; | &#x2705; | | | | | |
| stop_alarm | | | | | | | | | | &#x2705; |
\* The `set()` API allows multiple properties to be set in a single request.
## Hub (H100) Child Devices
&check; - Rust only\
&#x2705; - Rust and Python
| Feature<br/><br/> | KE100<br/><br/> | S200B<br/><br/> | T100<br/><br/> | T110<br/><br/> | T300<br/><br/> | T310<br/>T315 |
| -------------------------------- | :-------------- | :-------------- | :------------- | :------------- | :------------- | :------------ |
| get_device_info \* | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| get_device_info_json | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; | &#x2705; |
| get_temperature_humidity_records | | | | | | &#x2705; |
| get_trigger_logs | | &#x2705; | &#x2705; | &#x2705; | &#x2705; | |
| set_child_protection | &#x2705; | | | | | |
| set_frost_protection | &#x2705; | | | | | |
| set_max_control_temperature | &#x2705; | | | | | |
| set_min_control_temperature | &#x2705; | | | | | |
| set_target_temperature | &#x2705; | | | | | |
| set_temperature_offset | &#x2705; | | | | | |
\* Obtained by calling `get_child_device_list` on the hub device or `get_device_info` on a child device handler.
## Power Strips Child Devices
&check; - Rust only\
&#x2705; - Rust and Python
| Feature<br/><br/> | P300<br/>P306<br/> | P304M<br/>P316M<br/> |
| -------------------- | :----------------- | :------------------- |
| get_current_power | | &#x2705; |
| get_device_info \* | &#x2705; | &#x2705; |
| get_device_info_json | &#x2705; | &#x2705; |
| get_device_usage | | &#x2705; |
| get_energy_data | | &#x2705; |
| get_energy_usage | | &#x2705; |
| get_power_data | | &#x2705; |
| off | &#x2705; | &#x2705; |
| on | &#x2705; | &#x2705; |
\* Obtained by calling `get_child_device_list` on the hub device or `get_device_info` on a child device handler.

View File

@@ -0,0 +1,2 @@
max_width = 100
style_edition = "2024"

View File

@@ -0,0 +1,2 @@
[formatting]
column_width = 100

View File

@@ -0,0 +1,4 @@
__pycache__
.pytest_cache
**/*.so
**/*.pyd

View File

@@ -0,0 +1 @@
../CHANGELOG.md

View File

@@ -0,0 +1,32 @@
[package]
name = "tapo-py"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
publish = false
[lib]
name = "tapo"
crate-type = ["cdylib"]
doc = false
[features]
default = []
[dependencies]
anyhow = { workspace = true }
chrono = { workspace = true }
log = { workspace = true }
pyo3 = { workspace = true, features = [
"chrono",
"experimental-async",
"extension-module",
"py-clone",
] }
pyo3-async-runtimes = { version = "0.27", features = ["attributes", "tokio-runtime"] }
pyo3-log = { version = "0.13" }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "sync"] }
tapo = { path = "../tapo", features = ["python"] }

View File

@@ -0,0 +1 @@
../LICENSE

View File

@@ -0,0 +1 @@
../README.md

View File

@@ -0,0 +1,70 @@
"""Discover devices on the local network Example"""
import asyncio
import os
from tapo import ApiClient, DiscoveryResult
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
target = os.getenv("TARGET", "192.168.1.255")
timeout_s = int(os.getenv("TIMEOUT", 10))
print(f"Discovering Tapo devices on target: {target} for {timeout_s} seconds...")
api_client = ApiClient(tapo_username, tapo_password)
discovery = await api_client.discover_devices(target, timeout_s)
async for discovery_result in discovery:
try:
device = discovery_result.get()
match device:
case DiscoveryResult.GenericDevice(device_info, _handler):
print(
f"Found Unsupported Device '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.Light(device_info, _handler):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.ColorLight(device_info, _handler):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.RgbLightStrip(device_info, _handler):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.RgbicLightStrip(device_info, _handler):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.Plug(device_info, _handler):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.PlugEnergyMonitoring(device_info, _handler):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.PowerStrip(device_info, _handler):
print(
f"Found Power Strip of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.PowerStripEnergyMonitoring(device_info, _handler):
print(
f"Found Power Strip with Energy Monitoring of model '{device_info.model}' at IP address '{device_info.ip}'."
)
case DiscoveryResult.Hub(device_info, _handler):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
except Exception as e:
print(f"Error discovering device: {e}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,31 @@
"""Generic Device Example"""
import asyncio
import os
from tapo import ApiClient
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.generic_device(ip_address)
print("Turning device on...")
await device.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await device.off()
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,30 @@
"""Toggle Generic Device Example"""
import asyncio
import os
from tapo import ApiClient
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.generic_device(ip_address)
device_info = await device.get_device_info()
if device_info.device_on == True:
print("Device is on. Turning it off...")
await device.off()
elif device_info.device_on == False:
print("Device is off. Turning it on...")
await device.on()
else:
print("This device does not support on/off functionality.")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,120 @@
"""H100 Example"""
import asyncio
import os
from tapo import ApiClient
from tapo.requests import AlarmRingtone, AlarmVolume, AlarmDuration
from tapo.responses import KE100Result, S200BResult, T100Result, T110Result, T300Result, T31XResult
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
hub = await client.h100(ip_address)
device_info = await hub.get_device_info()
print(f"Device info: {device_info.to_dict()}")
child_device_list = await hub.get_child_device_list()
for child in child_device_list:
if child is None:
print("Found unsupported device.")
elif isinstance(child, KE100Result):
print(
"Found KE100 child device with nickname: {}, id: {}, current temperature: {:.2f} {} and target temperature: {:.2f} {}.".format(
child.nickname,
child.device_id,
child.current_temperature,
child.temperature_unit,
child.target_temperature,
child.temperature_unit,
)
)
elif isinstance(child, S200BResult):
s200b = await hub.s200b(device_id=child.device_id)
trigger_logs = await s200b.get_trigger_logs(5, 0)
print(
"Found S200B child device with nickname: {}, id: {}, last 5 trigger logs: {}.".format(
child.nickname,
child.device_id,
[log.to_dict() for log in trigger_logs.logs],
)
)
elif isinstance(child, T100Result):
t100 = await hub.t100(device_id=child.device_id)
trigger_logs = await t100.get_trigger_logs(5, 0)
print(
"Found T100 child device with nickname: {}, id: {}, detected: {}, last 5 trigger logs: {}.".format(
child.nickname,
child.device_id,
child.detected,
[log.to_dict() for log in trigger_logs.logs],
)
)
elif isinstance(child, T110Result):
t110 = await hub.t110(device_id=child.device_id)
trigger_logs = await t110.get_trigger_logs(5, 0)
print(
"Found T110 child device with nickname: {}, id: {}, open: {}, last 5 trigger logs: {}.".format(
child.nickname,
child.device_id,
child.open,
[log.to_dict() for log in trigger_logs.logs],
)
)
elif isinstance(child, T300Result):
t300 = await hub.t300(device_id=child.device_id)
trigger_logs = await t300.get_trigger_logs(5, 0)
print(
"Found T300 child device with nickname: {}, id: {}, in_alarm: {}, water_leak_status: {}, last 5 trigger logs: {}.".format(
child.nickname,
child.device_id,
child.in_alarm,
child.water_leak_status,
[log.to_dict() for log in trigger_logs.logs],
)
)
elif isinstance(child, T31XResult):
t31x = await hub.t315(device_id=child.device_id)
temperature_humidity_records = await t31x.get_temperature_humidity_records()
print(
"Found T31X child device with nickname: {}, id: {}, temperature: {:.2f} {}, humidity: {}%, earliest temperature and humidity record available: {}.".format(
child.nickname,
child.device_id,
child.current_temperature,
child.temperature_unit,
child.current_humidity,
(
temperature_humidity_records.records[0].to_dict()
if temperature_humidity_records.records
else None
),
)
)
print(f"Triggering the alarm ringtone 'Alarm 1' at a 'Low' volume for '3 Seconds'...")
await hub.play_alarm(AlarmRingtone.Alarm1, AlarmVolume.Low, AlarmDuration.Seconds, seconds=3)
device_info = await hub.get_device_info()
print(f"Is device ringing?: {device_info.in_alarm}")
print("Stopping the alarm after 1 Second...")
await asyncio.sleep(1)
await hub.stop_alarm()
device_info = await hub.get_device_info()
print(f"Is device ringing?: {device_info.in_alarm}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,40 @@
"""KE100 TRV Example"""
import asyncio
import os
from tapo import ApiClient
from tapo.requests import TemperatureUnitKE100
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
# Name of the KE100 device.
# Can be obtained from the Tapo App or by executing `get_child_device_component_list()` on the hub device.
device_name = os.getenv("DEVICE_NAME")
target_temperature = int(os.getenv("TARGET_TEMPERATURE"))
client = ApiClient(tapo_username, tapo_password)
hub = await client.h100(ip_address)
# Get a handler for the child device
device = await hub.ke100(nickname=device_name)
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
# Set target temperature.
# KE100 currently only supports Celsius as temperature unit.
print(f"Setting target temperature to {target_temperature} degrees Celsius...")
await device.set_target_temperature(target_temperature, TemperatureUnitKE100.Celsius)
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,40 @@
"""L510, L520 and L610 Example"""
import asyncio
import os
from tapo import ApiClient
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.l510(ip_address)
print("Turning device on...")
await device.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the brightness to 30%...")
await device.set_brightness(30)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await device.off()
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
device_usage = await device.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,62 @@
"""L530, L535 and L630 Example"""
import asyncio
import os
from tapo import ApiClient
from tapo.requests import Color
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.l530(ip_address)
print("Turning device on...")
await device.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the brightness to 30%...")
await device.set_brightness(30)
print("Setting the color to `Chocolate`...")
await device.set_color(Color.Chocolate)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the color to `Deep Sky Blue` using the `hue` and `saturation`...")
await device.set_hue_saturation(195, 100)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the color to `Incandescent` using the `color temperature`...")
await device.set_color_temperature(2700)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Using the `set` API to set multiple properties in a single request...")
await device.set().brightness(50).color(Color.HotPink).send(device)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await device.off()
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
device_usage = await device.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,62 @@
"""L900 Example"""
import asyncio
import os
from tapo import ApiClient
from tapo.requests import Color
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.l900(ip_address)
print("Turning device on...")
await device.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the brightness to 30%...")
await device.set_brightness(30)
print("Setting the color to `Chocolate`...")
await device.set_color(Color.Chocolate)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the color to `Deep Sky Blue` using the `hue` and `saturation`...")
await device.set_hue_saturation(195, 100)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the color to `Incandescent` using the `color temperature`...")
await device.set_color_temperature(2700)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Using the `set` API to set multiple properties in a single request...")
await device.set().brightness(50).color(Color.HotPink).send(device)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await device.off()
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
device_usage = await device.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,103 @@
"""L920 and L930 Example"""
import asyncio
import os
from tapo import ApiClient
from tapo.requests import Color, LightingEffect, LightingEffectPreset, LightingEffectType
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.l930(ip_address)
print("Turning device on...")
await device.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the brightness to 30%...")
await device.set_brightness(30)
print("Setting the color to `Chocolate`...")
await device.set_color(Color.Chocolate)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the color to `Deep Sky Blue` using the `hue` and `saturation`...")
await device.set_hue_saturation(195, 100)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting the color to `Incandescent` using the `color temperature`...")
await device.set_color_temperature(2700)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Using the `set` API to set multiple properties in a single request...")
await device.set().brightness(50).color(Color.HotPink).send(device)
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Setting a preset Lighting effect...")
await device.set_lighting_effect(LightingEffectPreset.BubblingCauldron)
print("Waiting 10 seconds...")
await asyncio.sleep(10)
print("Setting a custom static Lighting effect...")
custom_effect = (
LightingEffect(
"My Custom Static Effect", LightingEffectType.Static, True, True, 100, [(359, 85, 100)]
)
.with_expansion_strategy(1)
.with_segments([0, 1, 2])
.with_sequence([(359, 85, 100), (0, 0, 100), (236, 72, 100)])
)
await device.set_lighting_effect(custom_effect)
print("Waiting 10 seconds...")
await asyncio.sleep(10)
print("Setting a custom sequence Lighting effect...")
custom_effect = (
LightingEffect(
"My Custom Sequence Effect",
LightingEffectType.Sequence,
True,
True,
100,
[(359, 85, 100)],
)
.with_expansion_strategy(1)
.with_segments([0, 1, 2])
.with_sequence([(359, 85, 100), (0, 0, 100), (236, 72, 100)])
.with_direction(1)
.with_duration(50)
)
await device.set_lighting_effect(custom_effect)
print("Waiting 10 seconds...")
await asyncio.sleep(10)
print("Turning device off...")
await device.off()
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
device_usage = await device.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,34 @@
"""P100 and P105 Example"""
import asyncio
import os
from tapo import ApiClient
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.p100(ip_address)
print("Turning device on...")
await device.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await device.off()
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
device_usage = await device.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,118 @@
"""P110, P110M and P115 Example"""
import asyncio
import os
from datetime import datetime, timedelta, timezone
from tapo import ApiClient
from tapo.requests import EnergyDataInterval, PowerDataInterval
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
device = await client.p110(ip_address)
print("Turning device on...")
await device.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await device.off()
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
current_power = await device.get_current_power()
print(f"Current power: {current_power.to_dict()}")
device_usage = await device.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")
energy_usage = await device.get_energy_usage()
print(f"Energy usage: {energy_usage.to_dict()}")
today = datetime.now(timezone.utc)
# Energy data - Hourly interval
# `start_date` and `end_date` are an inclusive interval that must not be greater than 8 days.
energy_data_hourly = await device.get_energy_data(EnergyDataInterval.Hourly, today)
print(
"Energy data (hourly): "
f"Start date time '{energy_data_hourly.start_date_time}', "
f"Entries {len(energy_data_hourly.entries)}, "
f"First entry: {energy_data_hourly.entries[0].to_dict() if energy_data_hourly.entries else None}"
)
# Energy data - Daily interval
# `start_date` must be the first day of a quarter.
energy_data_daily = await device.get_energy_data(
EnergyDataInterval.Daily,
datetime(today.year, get_quarter_start_month(today), 1),
)
print(
"Energy data (daily): "
f"Start date time '{energy_data_daily.start_date_time}', "
f"Entries {len(energy_data_daily.entries)}, "
f"First entry: {energy_data_daily.entries[0].to_dict() if energy_data_daily.entries else None}"
)
# Energy data - Monthly interval
# `start_date` must be the first day of a year.
energy_data_monthly = await device.get_energy_data(
EnergyDataInterval.Monthly,
datetime(today.year, 1, 1),
)
print(
"Energy data (monthly): "
f"Start date time '{energy_data_monthly.start_date_time}', "
f"Entries {len(energy_data_monthly.entries)}, "
f"First entry: {energy_data_monthly.entries[0].to_dict() if energy_data_monthly.entries else None}"
)
# Power data - Every 5 minutes interval
# `start_date_time` and `end_date_time` describe an exclusive interval.
# If the result would yield more than 144 entries (i.e. 12 hours),
# the `end_date_time` will be adjusted to an earlier date and time.
power_data_every_5_minutes = await device.get_power_data(
PowerDataInterval.Every5Minutes,
today - timedelta(hours=12),
today,
)
print(
"Power data (every 5 minutes): "
f"Start date time '{power_data_every_5_minutes.start_date_time}', "
f"End date time '{power_data_every_5_minutes.end_date_time}', "
f"Entries {len(power_data_every_5_minutes.entries)}, "
f"First entry: {power_data_every_5_minutes.entries[0].to_dict() if power_data_every_5_minutes.entries else None}"
)
# Power data - Hourly interval
# `start_date_time` and `end_date_time` describe an exclusive interval.
# If the result would yield more than 144 entries (i.e. 6 days),
# the `end_date_time` will be adjusted to an earlier date and time.
power_data_hourly = await device.get_power_data(
PowerDataInterval.Hourly,
today - timedelta(days=3),
today,
)
print(
"Power data (hourly): "
f"Start date time '{power_data_hourly.start_date_time}', "
f"End date time '{power_data_hourly.end_date_time}', "
f"Entries {len(power_data_hourly.entries)}, "
f"First entry: {power_data_hourly.entries[0].to_dict() if power_data_hourly.entries else None}"
)
def get_quarter_start_month(today: datetime) -> int:
return ((today.month - 1) // 3) * 3 + 1
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,45 @@
"""P300 and P306 Example"""
import asyncio
import os
from tapo import ApiClient
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
power_strip = await client.p300(ip_address)
device_info = await power_strip.get_device_info()
print(f"Device info: {device_info.to_dict()}")
print("Getting child devices...")
child_device_list = await power_strip.get_child_device_list()
print(f"Found {len(child_device_list)} plugs")
for index, child in enumerate(child_device_list):
print(f"=== ({index + 1}) {child.nickname} ===")
print(f"Device ID: {child.device_id}")
print(f"State: {child.device_on}")
plug = await power_strip.plug(device_id=child.device_id)
print("Turning device on...")
await plug.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await plug.off()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,132 @@
"""P304M and P316M Example"""
import asyncio
from datetime import datetime, timedelta, timezone
import os
from tapo import ApiClient
from tapo.requests import EnergyDataInterval, PowerDataInterval
async def main():
tapo_username = os.getenv("TAPO_USERNAME")
tapo_password = os.getenv("TAPO_PASSWORD")
ip_address = os.getenv("IP_ADDRESS")
client = ApiClient(tapo_username, tapo_password)
power_strip = await client.p304(ip_address)
device_info = await power_strip.get_device_info()
print(f"Device info: {device_info.to_dict()}")
print("Getting child devices...")
child_device_list = await power_strip.get_child_device_list()
print(f"Found {len(child_device_list)} plugs")
for index, child in enumerate(child_device_list):
print(f"=== ({index + 1}) {child.nickname} ===")
print(f"Device ID: {child.device_id}")
print(f"State: {child.device_on}")
plug = await power_strip.plug(device_id=child.device_id)
print("Turning device on...")
await plug.on()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
print("Turning device off...")
await plug.off()
print("Waiting 2 seconds...")
await asyncio.sleep(2)
current_power = await plug.get_current_power()
print(f"Current power: {current_power.to_dict()}")
device_usage = await plug.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")
energy_usage = await plug.get_energy_usage()
print(f"Energy usage: {energy_usage.to_dict()}")
today = datetime.now(timezone.utc)
# Energy data - Hourly interval
# `start_date` and `end_date` are an inclusive interval that must not be greater than 8 days.
energy_data_hourly = await plug.get_energy_data(EnergyDataInterval.Hourly, today)
print(
"Energy data (hourly): "
f"Start date time '{energy_data_hourly.start_date_time}', "
f"Entries {len(energy_data_hourly.entries)}, "
f"First entry: {energy_data_hourly.entries[0].to_dict() if energy_data_hourly.entries else None}"
)
# Energy data - Daily interval
# `start_date` must be the first day of a quarter.
energy_data_daily = await plug.get_energy_data(
EnergyDataInterval.Daily,
datetime(today.year, get_quarter_start_month(today), 1),
)
print(
"Energy data (daily): "
f"Start date time '{energy_data_daily.start_date_time}', "
f"Entries {len(energy_data_daily.entries)}, "
f"First entry: {energy_data_daily.entries[0].to_dict() if energy_data_daily.entries else None}"
)
# Energy data - Monthly interval
# `start_date` must be the first day of a year.
energy_data_monthly = await plug.get_energy_data(
EnergyDataInterval.Monthly,
datetime(today.year, 1, 1),
)
print(
"Energy data (monthly): "
f"Start date time '{energy_data_monthly.start_date_time}', "
f"Entries {len(energy_data_monthly.entries)}, "
f"First entry: {energy_data_monthly.entries[0].to_dict() if energy_data_monthly.entries else None}"
)
# Power data - Every 5 minutes interval
# `start_date_time` and `end_date_time` describe an exclusive interval.
# If the result would yield more than 144 entries (i.e. 12 hours),
# the `end_date_time` will be adjusted to an earlier date and time.
power_data_every_5_minutes = await plug.get_power_data(
PowerDataInterval.Every5Minutes,
today - timedelta(hours=12),
today,
)
print(
"Power data (every 5 minutes): "
f"Start date time '{power_data_every_5_minutes.start_date_time}', "
f"End date time '{power_data_every_5_minutes.end_date_time}', "
f"Entries {len(power_data_every_5_minutes.entries)}, "
f"First entry: {power_data_every_5_minutes.entries[0].to_dict() if power_data_every_5_minutes.entries else None}"
)
# Power data - Hourly interval
# `start_date_time` and `end_date_time` describe an exclusive interval.
# If the result would yield more than 144 entries (i.e. 6 days),
# the `end_date_time` will be adjusted to an earlier date and time.
power_data_hourly = await plug.get_power_data(
PowerDataInterval.Hourly,
today - timedelta(days=3),
today,
)
print(
"Power data (hourly): "
f"Start date time '{power_data_hourly.start_date_time}', "
f"End date time '{power_data_hourly.end_date_time}', "
f"Entries {len(power_data_hourly.entries)}, "
f"First entry: {power_data_hourly.entries[0].to_dict() if power_data_hourly.entries else None}"
)
def get_quarter_start_month(today: datetime) -> int:
return ((today.month - 1) // 3) * 3 + 1
if __name__ == "__main__":
asyncio.run(main())

180
agents/tapo/tapo-fork/tapo-py/poetry.lock generated Normal file
View File

@@ -0,0 +1,180 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
[[package]]
name = "black"
version = "25.11.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e"},
{file = "black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0"},
{file = "black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37"},
{file = "black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03"},
{file = "black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a"},
{file = "black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170"},
{file = "black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc"},
{file = "black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e"},
{file = "black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac"},
{file = "black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96"},
{file = "black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd"},
{file = "black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409"},
{file = "black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b"},
{file = "black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd"},
{file = "black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993"},
{file = "black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c"},
{file = "black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170"},
{file = "black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545"},
{file = "black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda"},
{file = "black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664"},
{file = "black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06"},
{file = "black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2"},
{file = "black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc"},
{file = "black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc"},
{file = "black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b"},
{file = "black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
pytokens = ">=0.3.0"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.3.1"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
{file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["dev"]
markers = "platform_system == \"Windows\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "maturin"
version = "1.10.2"
description = "Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "maturin-1.10.2-py3-none-linux_armv6l.whl", hash = "sha256:11c73815f21a755d2129c410e6cb19dbfacbc0155bfc46c706b69930c2eb794b"},
{file = "maturin-1.10.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7fbd997c5347649ee7987bd05a92bd5b8b07efa4ac3f8bcbf6196e07eb573d89"},
{file = "maturin-1.10.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3ce9b2ad4fb9c341f450a6d32dc3edb409a2d582a81bc46ba55f6e3b6196b22"},
{file = "maturin-1.10.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:f0d1b7b5f73c8d30a7e71cd2a2189a7f0126a3a3cd8b3d6843e7e1d4db50f759"},
{file = "maturin-1.10.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:efcd496a3202ffe0d0489df1f83d08b91399782fb2dd545d5a1e7bf6fd81af39"},
{file = "maturin-1.10.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a41ec70d99e27c05377be90f8e3c3def2a7bae4d0d9d5ea874aaf2d1da625d5c"},
{file = "maturin-1.10.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:07a82864352feeaf2167247c8206937ef6c6ae9533025d416b7004ade0ea601d"},
{file = "maturin-1.10.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:04df81ee295dcda37828bd025a4ac688ea856e3946e4cb300a8f44a448de0069"},
{file = "maturin-1.10.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96e1d391e4c1fa87edf2a37e4d53d5f2e5f39dd880b9d8306ac9f8eb212d23f8"},
{file = "maturin-1.10.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a217aa7c42aa332fb8e8377eb07314e1f02cf0fe036f614aca4575121952addd"},
{file = "maturin-1.10.2-py3-none-win32.whl", hash = "sha256:da031771d9fb6ddb1d373638ec2556feee29e4507365cd5749a2d354bcadd818"},
{file = "maturin-1.10.2-py3-none-win_amd64.whl", hash = "sha256:da777766fd584440dc9fecd30059a94f85e4983f58b09e438ae38ee4b494024c"},
{file = "maturin-1.10.2-py3-none-win_arm64.whl", hash = "sha256:a4c29a770ea2c76082e0afc6d4efd8ee94405588bfae00d10828f72e206c739b"},
{file = "maturin-1.10.2.tar.gz", hash = "sha256:259292563da89850bf8f7d37aa4ddba22905214c1e180b1c8f55505dfd8c0e81"},
]
[package.extras]
patchelf = ["patchelf"]
zig = ["ziglang (>=0.10.0,<0.13.0)"]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
]
[[package]]
name = "packaging"
version = "25.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.5.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"},
{file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"},
]
[package.extras]
docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"]
type = ["mypy (>=1.18.2)"]
[[package]]
name = "pytokens"
version = "0.3.0"
description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3"},
{file = "pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a"},
]
[package.extras]
dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
[metadata]
lock-version = "2.1"
python-versions = "^3.11"
content-hash = "fdca2529430378024bd7592d1cc9a036dce731d069014afa9422296fe281758a"

View File

View File

@@ -0,0 +1,55 @@
[tool.poetry]
name = "tapo"
version = "0.8.8"
description = "Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L535, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P110M, P115), power strips (P300, P304M, P306, P316M), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315)."
authors = ["Mihai Dinculescu <mihai.dinculescu@outlook.com>"]
[project]
name = "tapo"
version = "0.8.8"
description = "Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with light bulbs (L510, L520, L530, L535, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P110M, P115), power strips (P300, P304M, P306, P316M), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315)."
readme = "README.md"
license = "MIT"
authors = [{ name = "Mihai Dinculescu", email = "mihai.dinculescu@outlook.com" }]
maintainers = [{ name = "Mihai Dinculescu", email = "mihai.dinculescu@outlook.com" }]
keywords = ["Tapo", "TP-Link", "Smart Home", "Home Automation", "IoT"]
classifiers = [
"Development Status :: 4 - Beta",
"Topic :: Software Development :: Embedded Systems",
"Environment :: Console",
"Operating System :: OS Independent",
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Topic :: Home Automation",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
requires-python = ">=3.11"
[project.urls]
Changelog = 'https://github.com/mihai-dinculescu/tapo/blob/main/CHANGELOG.md'
Funding = 'https://github.com/mihai-dinculescu'
Homepage = 'https://github.com/mihai-dinculescu/tapo'
Source = 'https://github.com/mihai-dinculescu/tapo'
[tool.poetry.dependencies]
python = "^3.11"
[tool.poetry.group.dev.dependencies]
maturin = ">=1.0,<2.0"
black = ">=25.0,<26.0"
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[tool.maturin]
python-source = "tapo-py"
bindings = 'pyo3'
features = ["pyo3/extension-module"]
include = ["README.md", "CHANGELOG.md", "LICENSE", "tapo-py/tapo-py/tapo/*"]
[tool.black]
line-length = 100

View File

@@ -0,0 +1,29 @@
mod api_client;
mod child_devices;
mod color_light_handler;
mod discovery;
mod generic_device_handler;
mod hub_handler;
mod light_handler;
mod plug_energy_monitoring_handler;
mod plug_handler;
mod power_strip_energy_monitoring_handler;
mod power_strip_handler;
mod py_handler_ext;
mod rgb_light_strip_handler;
mod rgbic_light_strip_handler;
pub use api_client::*;
pub use child_devices::*;
pub use color_light_handler::*;
pub use discovery::*;
pub use generic_device_handler::*;
pub use hub_handler::*;
pub use light_handler::*;
pub use plug_energy_monitoring_handler::*;
pub use plug_handler::*;
pub use power_strip_energy_monitoring_handler::*;
pub use power_strip_handler::*;
pub use py_handler_ext::*;
pub use rgb_light_strip_handler::*;
pub use rgbic_light_strip_handler::*;

View File

@@ -0,0 +1,165 @@
use std::time::Duration;
use pyo3::prelude::*;
use tapo::{
ApiClient, ColorLightHandler, DeviceDiscovery, GenericDeviceHandler, HubHandler, LightHandler,
PlugEnergyMonitoringHandler, PlugHandler, PowerStripEnergyMonitoringHandler, PowerStripHandler,
RgbLightStripHandler, RgbicLightStripHandler,
};
use crate::call_handler_constructor;
use crate::errors::ErrorWrapper;
use super::{
PyColorLightHandler, PyDeviceDiscovery, PyGenericDeviceHandler, PyHubHandler, PyLightHandler,
PyPlugEnergyMonitoringHandler, PyPlugHandler, PyPowerStripEnergyMonitoringHandler,
PyPowerStripHandler, PyRgbLightStripHandler, PyRgbicLightStripHandler,
};
#[pyclass(name = "ApiClient")]
pub struct PyApiClient {
client: ApiClient,
}
#[pymethods]
impl PyApiClient {
#[new]
#[pyo3(signature = (tapo_username, tapo_password, timeout_s=None))]
pub fn new(
tapo_username: String,
tapo_password: String,
timeout_s: Option<u64>,
) -> Result<Self, ErrorWrapper> {
let client = match timeout_s {
Some(timeout_s) => ApiClient::new(tapo_username, tapo_password)
.with_timeout(Duration::from_secs(timeout_s)),
None => ApiClient::new(tapo_username, tapo_password),
};
Ok(Self { client })
}
pub async fn discover_devices(
&self,
target: String,
timeout_s: u64,
) -> Result<PyDeviceDiscovery, ErrorWrapper> {
let discovery: DeviceDiscovery =
call_handler_constructor!(self, tapo::ApiClient::discover_devices, target, timeout_s);
Ok(PyDeviceDiscovery::new(discovery))
}
pub async fn generic_device(&self, ip_address: String) -> PyResult<PyGenericDeviceHandler> {
let handler: GenericDeviceHandler =
call_handler_constructor!(self, tapo::ApiClient::generic_device, ip_address);
Ok(PyGenericDeviceHandler::new(handler))
}
pub async fn l510(&self, ip_address: String) -> PyResult<PyLightHandler> {
let handler: LightHandler =
call_handler_constructor!(self, tapo::ApiClient::l510, ip_address);
Ok(PyLightHandler::new(handler))
}
pub async fn l520(&self, ip_address: String) -> PyResult<PyLightHandler> {
let handler: LightHandler =
call_handler_constructor!(self, tapo::ApiClient::l520, ip_address);
Ok(PyLightHandler::new(handler))
}
pub async fn l530(&self, ip_address: String) -> PyResult<PyColorLightHandler> {
let handler: ColorLightHandler =
call_handler_constructor!(self, tapo::ApiClient::l530, ip_address);
Ok(PyColorLightHandler::new(handler))
}
pub async fn l535(&self, ip_address: String) -> PyResult<PyColorLightHandler> {
let handler: ColorLightHandler =
call_handler_constructor!(self, tapo::ApiClient::l535, ip_address);
Ok(PyColorLightHandler::new(handler))
}
pub async fn l610(&self, ip_address: String) -> PyResult<PyLightHandler> {
let handler: LightHandler =
call_handler_constructor!(self, tapo::ApiClient::l610, ip_address);
Ok(PyLightHandler::new(handler))
}
pub async fn l630(&self, ip_address: String) -> PyResult<PyColorLightHandler> {
let handler: ColorLightHandler =
call_handler_constructor!(self, tapo::ApiClient::l630, ip_address);
Ok(PyColorLightHandler::new(handler))
}
pub async fn l900(&self, ip_address: String) -> PyResult<PyRgbLightStripHandler> {
let handler: RgbLightStripHandler =
call_handler_constructor!(self, tapo::ApiClient::l900, ip_address);
Ok(PyRgbLightStripHandler::new(handler))
}
pub async fn l920(&self, ip_address: String) -> PyResult<PyRgbicLightStripHandler> {
let handler: RgbicLightStripHandler =
call_handler_constructor!(self, tapo::ApiClient::l920, ip_address);
Ok(PyRgbicLightStripHandler::new(handler))
}
pub async fn l930(&self, ip_address: String) -> PyResult<PyRgbicLightStripHandler> {
let handler: RgbicLightStripHandler =
call_handler_constructor!(self, tapo::ApiClient::l930, ip_address);
Ok(PyRgbicLightStripHandler::new(handler))
}
pub async fn p100(&self, ip_address: String) -> PyResult<PyPlugHandler> {
let handler: PlugHandler =
call_handler_constructor!(self, tapo::ApiClient::p100, ip_address);
Ok(PyPlugHandler::new(handler))
}
pub async fn p105(&self, ip_address: String) -> PyResult<PyPlugHandler> {
let handler: PlugHandler =
call_handler_constructor!(self, tapo::ApiClient::p105, ip_address);
Ok(PyPlugHandler::new(handler))
}
pub async fn p110(&self, ip_address: String) -> PyResult<PyPlugEnergyMonitoringHandler> {
let handler: PlugEnergyMonitoringHandler =
call_handler_constructor!(self, tapo::ApiClient::p110, ip_address);
Ok(PyPlugEnergyMonitoringHandler::new(handler))
}
pub async fn p115(&self, ip_address: String) -> PyResult<PyPlugEnergyMonitoringHandler> {
let handler: PlugEnergyMonitoringHandler =
call_handler_constructor!(self, tapo::ApiClient::p115, ip_address);
Ok(PyPlugEnergyMonitoringHandler::new(handler))
}
pub async fn p300(&self, ip_address: String) -> PyResult<PyPowerStripHandler> {
let handler: PowerStripHandler =
call_handler_constructor!(self, tapo::ApiClient::p300, ip_address);
Ok(PyPowerStripHandler::new(handler))
}
pub async fn p304(&self, ip_address: String) -> PyResult<PyPowerStripEnergyMonitoringHandler> {
let handler: PowerStripEnergyMonitoringHandler =
call_handler_constructor!(self, tapo::ApiClient::p304, ip_address);
Ok(PyPowerStripEnergyMonitoringHandler::new(handler))
}
pub async fn p306(&self, ip_address: String) -> PyResult<PyPowerStripHandler> {
let handler: PowerStripHandler =
call_handler_constructor!(self, tapo::ApiClient::p306, ip_address);
Ok(PyPowerStripHandler::new(handler))
}
pub async fn p316(&self, ip_address: String) -> PyResult<PyPowerStripEnergyMonitoringHandler> {
let handler: PowerStripEnergyMonitoringHandler =
call_handler_constructor!(self, tapo::ApiClient::p316, ip_address);
Ok(PyPowerStripEnergyMonitoringHandler::new(handler))
}
pub async fn h100(&self, ip_address: String) -> PyResult<PyHubHandler> {
let handler: HubHandler =
call_handler_constructor!(self, tapo::ApiClient::h100, ip_address);
Ok(PyHubHandler::new(handler))
}
}

View File

@@ -0,0 +1,17 @@
mod ke100_handler;
mod power_strip_plug_energy_monitoring_handler;
mod power_strip_plug_handler;
mod s200b_handler;
mod t100_handler;
mod t110_handler;
mod t300_handler;
mod t31x_handler;
pub use ke100_handler::*;
pub use power_strip_plug_energy_monitoring_handler::*;
pub use power_strip_plug_handler::*;
pub use s200b_handler::*;
pub use t31x_handler::*;
pub use t100_handler::*;
pub use t110_handler::*;
pub use t300_handler::*;

View File

@@ -0,0 +1,101 @@
use std::{ops::Deref, sync::Arc};
use pyo3::{prelude::*, types::PyDict};
use tapo::KE100Handler;
use tapo::responses::{KE100Result, TemperatureUnitKE100};
use crate::call_handler_method;
#[derive(Clone)]
#[pyclass(name = "KE100Handler")]
pub struct PyKE100Handler {
inner: Arc<KE100Handler>,
}
impl PyKE100Handler {
pub fn new(handler: KE100Handler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyKE100Handler {
pub async fn get_device_info(&self) -> PyResult<KE100Result> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), KE100Handler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(handler.deref(), KE100Handler::get_device_info_json)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn set_child_protection(&self, on: bool) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), KE100Handler::set_child_protection, on)
}
pub async fn set_frost_protection(&self, on: bool) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), KE100Handler::set_frost_protection, on)
}
pub async fn set_max_control_temperature(
&self,
value: u8,
unit: TemperatureUnitKE100,
) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
KE100Handler::set_max_control_temperature,
value,
unit
)
}
pub async fn set_min_control_temperature(
&self,
value: u8,
unit: TemperatureUnitKE100,
) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
KE100Handler::set_min_control_temperature,
value,
unit
)
}
pub async fn set_target_temperature(
&self,
value: u8,
unit: TemperatureUnitKE100,
) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
KE100Handler::set_target_temperature,
value,
unit
)
}
pub async fn set_temperature_offset(
&self,
value: i8,
unit: TemperatureUnitKE100,
) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
KE100Handler::set_temperature_offset,
value,
unit
)
}
}

View File

@@ -0,0 +1,132 @@
use std::{ops::Deref, sync::Arc};
use chrono::{DateTime, NaiveDate, Utc};
use pyo3::{prelude::*, types::PyDict};
use tapo::PowerStripPlugEnergyMonitoringHandler;
use tapo::requests::{EnergyDataInterval, PowerDataInterval};
use tapo::responses::{
CurrentPowerResult, DeviceUsageEnergyMonitoringResult, EnergyDataResult, EnergyUsageResult,
PowerDataResult, PowerStripPlugEnergyMonitoringResult,
};
use crate::call_handler_method;
use crate::requests::{PyEnergyDataInterval, PyPowerDataInterval};
#[derive(Clone)]
#[pyclass(name = "PowerStripPlugEnergyMonitoringHandler")]
pub struct PyPowerStripPlugEnergyMonitoringHandler {
inner: Arc<PowerStripPlugEnergyMonitoringHandler>,
}
impl PyPowerStripPlugEnergyMonitoringHandler {
pub fn new(handler: PowerStripPlugEnergyMonitoringHandler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyPowerStripPlugEnergyMonitoringHandler {
pub async fn get_device_info(&self) -> PyResult<PowerStripPlugEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
PowerStripPlugEnergyMonitoringHandler::get_device_info
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.deref(),
PowerStripPlugEnergyMonitoringHandler::get_device_info_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), PowerStripPlugEnergyMonitoringHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), PowerStripPlugEnergyMonitoringHandler::off)
}
pub async fn get_current_power(&self) -> PyResult<CurrentPowerResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
PowerStripPlugEnergyMonitoringHandler::get_current_power,
)
}
pub async fn get_device_usage(&self) -> PyResult<DeviceUsageEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
PowerStripPlugEnergyMonitoringHandler::get_device_usage,
)
}
pub async fn get_energy_usage(&self) -> PyResult<EnergyUsageResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
PowerStripPlugEnergyMonitoringHandler::get_energy_usage,
)
}
#[pyo3(signature = (interval, start_date, end_date=None))]
pub async fn get_energy_data(
&self,
interval: PyEnergyDataInterval,
start_date: NaiveDate,
end_date: Option<NaiveDate>,
) -> PyResult<EnergyDataResult> {
let interval = match interval {
PyEnergyDataInterval::Hourly => EnergyDataInterval::Hourly {
start_date,
end_date: end_date.unwrap_or(start_date),
},
PyEnergyDataInterval::Daily => EnergyDataInterval::Daily { start_date },
PyEnergyDataInterval::Monthly => EnergyDataInterval::Monthly { start_date },
};
let handler = self.inner.clone();
let result = call_handler_method!(
handler.deref(),
PowerStripPlugEnergyMonitoringHandler::get_energy_data,
interval
)?;
Ok(result)
}
pub async fn get_power_data(
&self,
interval: PyPowerDataInterval,
start_date_time: DateTime<Utc>,
end_date_time: DateTime<Utc>,
) -> PyResult<PowerDataResult> {
let interval = match interval {
PyPowerDataInterval::Every5Minutes => PowerDataInterval::Every5Minutes {
start_date_time,
end_date_time,
},
PyPowerDataInterval::Hourly => PowerDataInterval::Hourly {
start_date_time,
end_date_time,
},
};
let handler = self.inner.clone();
let result = call_handler_method!(
handler.deref(),
PowerStripPlugEnergyMonitoringHandler::get_power_data,
interval
)?;
Ok(result)
}
}

View File

@@ -0,0 +1,46 @@
use std::{ops::Deref, sync::Arc};
use pyo3::{prelude::*, types::PyDict};
use tapo::PowerStripPlugHandler;
use tapo::responses::PowerStripPlugResult;
use crate::call_handler_method;
#[derive(Clone)]
#[pyclass(name = "PowerStripPlugHandler")]
pub struct PyPowerStripPlugHandler {
inner: Arc<PowerStripPlugHandler>,
}
impl PyPowerStripPlugHandler {
pub fn new(handler: PowerStripPlugHandler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyPowerStripPlugHandler {
pub async fn get_device_info(&self) -> PyResult<PowerStripPlugResult> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), PowerStripPlugHandler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result =
call_handler_method!(handler.deref(), PowerStripPlugHandler::get_device_info_json)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), PowerStripPlugHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), PowerStripPlugHandler::off)
}
}

View File

@@ -0,0 +1,51 @@
use std::{ops::Deref, sync::Arc};
use pyo3::{prelude::*, types::PyDict};
use tapo::S200BHandler;
use tapo::responses::S200BResult;
use crate::call_handler_method;
use crate::responses::TriggerLogsS200BResult;
#[derive(Clone)]
#[pyclass(name = "S200BHandler")]
pub struct PyS200BHandler {
inner: Arc<S200BHandler>,
}
impl PyS200BHandler {
pub fn new(handler: S200BHandler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyS200BHandler {
pub async fn get_device_info(&self) -> PyResult<S200BResult> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), S200BHandler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(handler.deref(), S200BHandler::get_device_info_json)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_trigger_logs(
&self,
page_size: u64,
start_id: u64,
) -> PyResult<TriggerLogsS200BResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
S200BHandler::get_trigger_logs,
page_size,
start_id
)
.map(|result| result.into())
}
}

View File

@@ -0,0 +1,50 @@
use std::{ops::Deref, sync::Arc};
use pyo3::{prelude::*, types::PyDict};
use tapo::{T100Handler, responses::T100Result};
use crate::call_handler_method;
use crate::responses::TriggerLogsT100Result;
#[derive(Clone)]
#[pyclass(name = "T100Handler")]
pub struct PyT100Handler {
inner: Arc<T100Handler>,
}
impl PyT100Handler {
pub fn new(handler: T100Handler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyT100Handler {
pub async fn get_device_info(&self) -> PyResult<T100Result> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), T100Handler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(handler.deref(), T100Handler::get_device_info_json)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_trigger_logs(
&self,
page_size: u64,
start_id: u64,
) -> PyResult<TriggerLogsT100Result> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
T100Handler::get_trigger_logs,
page_size,
start_id
)
.map(|result| result.into())
}
}

View File

@@ -0,0 +1,51 @@
use std::{ops::Deref, sync::Arc};
use pyo3::{prelude::*, types::PyDict};
use tapo::T110Handler;
use tapo::responses::T110Result;
use crate::call_handler_method;
use crate::responses::TriggerLogsT110Result;
#[derive(Clone)]
#[pyclass(name = "T110Handler")]
pub struct PyT110Handler {
inner: Arc<T110Handler>,
}
impl PyT110Handler {
pub fn new(handler: T110Handler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyT110Handler {
pub async fn get_device_info(&self) -> PyResult<T110Result> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), T110Handler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(handler.deref(), T110Handler::get_device_info_json)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_trigger_logs(
&self,
page_size: u64,
start_id: u64,
) -> PyResult<TriggerLogsT110Result> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
T110Handler::get_trigger_logs,
page_size,
start_id
)
.map(|result| result.into())
}
}

View File

@@ -0,0 +1,51 @@
use std::{ops::Deref, sync::Arc};
use pyo3::{prelude::*, types::PyDict};
use tapo::T300Handler;
use tapo::responses::T300Result;
use crate::call_handler_method;
use crate::responses::TriggerLogsT300Result;
#[derive(Clone)]
#[pyclass(name = "T300Handler")]
pub struct PyT300Handler {
inner: Arc<T300Handler>,
}
impl PyT300Handler {
pub fn new(handler: T300Handler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyT300Handler {
pub async fn get_device_info(&self) -> PyResult<T300Result> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), T300Handler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(handler.deref(), T300Handler::get_device_info_json)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_trigger_logs(
&self,
page_size: u64,
start_id: u64,
) -> PyResult<TriggerLogsT300Result> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
T300Handler::get_trigger_logs,
page_size,
start_id
)
.map(|result| result.into())
}
}

View File

@@ -0,0 +1,43 @@
use std::{ops::Deref, sync::Arc};
use pyo3::{prelude::*, types::PyDict};
use tapo::T31XHandler;
use tapo::responses::{T31XResult, TemperatureHumidityRecords};
use crate::call_handler_method;
#[derive(Clone)]
#[pyclass(name = "T31XHandler")]
pub struct PyT31XHandler {
inner: Arc<T31XHandler>,
}
impl PyT31XHandler {
pub fn new(handler: T31XHandler) -> Self {
Self {
inner: Arc::new(handler),
}
}
}
#[pymethods]
impl PyT31XHandler {
pub async fn get_device_info(&self) -> PyResult<T31XResult> {
let handler = self.inner.clone();
call_handler_method!(handler.deref(), T31XHandler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(handler.deref(), T31XHandler::get_device_info_json)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_temperature_humidity_records(&self) -> PyResult<TemperatureHumidityRecords> {
let handler = self.inner.clone();
call_handler_method!(
handler.deref(),
T31XHandler::get_temperature_humidity_records
)
}
}

View File

@@ -0,0 +1,138 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use tapo::requests::Color;
use tapo::responses::{DeviceInfoColorLightResult, DeviceUsageEnergyMonitoringResult};
use tapo::{ColorLightHandler, DeviceManagementExt as _, HandlerExt};
use tokio::sync::RwLock;
use crate::api::PyHandlerExt;
use crate::call_handler_method;
use crate::requests::PyColorLightSetDeviceInfoParams;
#[derive(Clone)]
#[pyclass(name = "ColorLightHandler")]
pub struct PyColorLightHandler {
inner: Arc<RwLock<ColorLightHandler>>,
}
impl PyColorLightHandler {
pub fn new(handler: ColorLightHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
}
impl PyHandlerExt for PyColorLightHandler {
fn get_inner_handler(&self) -> Arc<RwLock<impl HandlerExt + 'static>> {
Arc::clone(&self.inner)
}
}
#[pymethods]
impl PyColorLightHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
ColorLightHandler::refresh_session,
discard_result
)
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), ColorLightHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), ColorLightHandler::off)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::device_reset,
)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoColorLightResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::get_device_info
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::get_device_info_json,
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_device_usage(&self) -> PyResult<DeviceUsageEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::get_device_usage
)
}
pub fn set(&self) -> PyColorLightSetDeviceInfoParams {
PyColorLightSetDeviceInfoParams::new()
}
pub async fn set_brightness(&self, brightness: u8) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::set_brightness,
brightness
)
}
pub async fn set_color(&self, color: Color) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::set_color,
color
)
}
pub async fn set_hue_saturation(&self, hue: u16, saturation: u8) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::set_hue_saturation,
hue,
saturation
)
}
pub async fn set_color_temperature(&self, color_temperature: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
ColorLightHandler::set_color_temperature,
color_temperature
)
}
}

View File

@@ -0,0 +1,5 @@
mod device_discovery;
mod discovery_result;
pub use device_discovery::*;
pub use discovery_result::*;

View File

@@ -0,0 +1,87 @@
use std::sync::Arc;
use pyo3::prelude::*;
use tapo::{DeviceDiscovery, StreamExt as _};
use tokio::sync::Mutex;
use super::{PyMaybeDiscoveryResult, convert_result_to_maybe_py};
#[derive(Clone)]
#[pyclass(name = "DeviceDiscovery")]
pub struct PyDeviceDiscovery {
pub inner: Arc<Mutex<DeviceDiscovery>>,
}
impl PyDeviceDiscovery {
pub fn new(inner: DeviceDiscovery) -> Self {
Self {
inner: Arc::new(Mutex::new(inner)),
}
}
}
#[pymethods]
impl PyDeviceDiscovery {
fn __iter__(slf: PyRef<'_, Self>) -> PyResult<PyDeviceDiscoveryIter> {
Ok(PyDeviceDiscoveryIter {
inner: (*slf).inner.clone(),
})
}
fn __aiter__(slf: PyRef<'_, Self>) -> PyResult<PyDeviceDiscoveryIter> {
Ok(PyDeviceDiscoveryIter {
inner: (*slf).inner.clone(),
})
}
}
#[pyclass(name = "DeviceDiscoveryIter")]
pub struct PyDeviceDiscoveryIter {
pub inner: Arc<Mutex<DeviceDiscovery>>,
}
#[pymethods]
impl PyDeviceDiscoveryIter {
fn __iter__(slf: Py<Self>) -> Py<Self> {
slf
}
fn __aiter__(slf: Py<Self>) -> Py<Self> {
slf
}
fn __next__(slf: PyRefMut<'_, Self>) -> PyResult<Option<PyMaybeDiscoveryResult>> {
let inner = (*slf).inner.clone();
let result = Python::attach(|py| {
py.detach(|| {
crate::runtime::tokio().block_on(async {
let mut guard = inner.lock().await;
guard.next().await
})
})
});
if let Some(result) = result {
let result_maybe_py = convert_result_to_maybe_py(result)?;
Ok(Some(result_maybe_py))
} else {
Ok(None)
}
}
fn __anext__<'py>(slf: PyRefMut<'_, Self>, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
let inner = (*slf).inner.clone();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let mut guard = inner.lock().await;
let result = guard.next().await;
match result {
Some(result) => convert_result_to_maybe_py(result),
None => Err(PyErr::new::<pyo3::exceptions::PyStopAsyncIteration, _>(
"No more devices found",
)),
}
})
}
}

View File

@@ -0,0 +1,170 @@
use pyo3::prelude::*;
use tapo::responses::{
DeviceInfoColorLightResult, DeviceInfoGenericResult, DeviceInfoHubResult,
DeviceInfoLightResult, DeviceInfoPlugEnergyMonitoringResult, DeviceInfoPlugResult,
DeviceInfoPowerStripResult, DeviceInfoRgbLightStripResult, DeviceInfoRgbicLightStripResult,
};
use tapo::{DiscoveryResult, Error};
use crate::api::{
PyColorLightHandler, PyGenericDeviceHandler, PyHubHandler, PyLightHandler,
PyPlugEnergyMonitoringHandler, PyPlugHandler, PyPowerStripEnergyMonitoringHandler,
PyPowerStripHandler, PyRgbLightStripHandler, PyRgbicLightStripHandler,
};
use crate::errors::ErrorWrapper;
#[pyclass(name = "DiscoveryResult")]
#[allow(clippy::large_enum_variant)]
pub enum PyDiscoveryResult {
GenericDevice {
device_info: DeviceInfoGenericResult,
handler: PyGenericDeviceHandler,
},
Light {
device_info: DeviceInfoLightResult,
handler: PyLightHandler,
},
ColorLight {
device_info: DeviceInfoColorLightResult,
handler: PyColorLightHandler,
},
RgbLightStrip {
device_info: DeviceInfoRgbLightStripResult,
handler: PyRgbLightStripHandler,
},
RgbicLightStrip {
device_info: DeviceInfoRgbicLightStripResult,
handler: PyRgbicLightStripHandler,
},
Plug {
device_info: DeviceInfoPlugResult,
handler: PyPlugHandler,
},
PlugEnergyMonitoring {
device_info: DeviceInfoPlugEnergyMonitoringResult,
handler: PyPlugEnergyMonitoringHandler,
},
PowerStrip {
device_info: DeviceInfoPowerStripResult,
handler: PyPowerStripHandler,
},
PowerStripEnergyMonitoring {
device_info: DeviceInfoPowerStripResult,
handler: PyPowerStripEnergyMonitoringHandler,
},
Hub {
device_info: DeviceInfoHubResult,
handler: PyHubHandler,
},
}
#[pyclass(name = "MaybeDiscoveryResult")]
pub struct PyMaybeDiscoveryResult {
result: Option<PyDiscoveryResult>,
exception: Option<ErrorWrapper>,
}
#[pymethods]
impl PyMaybeDiscoveryResult {
pub fn get(mut slf: PyRefMut<'_, Self>) -> PyResult<PyDiscoveryResult> {
if let Some(result) = slf.result.take() {
Ok(result)
} else if let Some(exception) = slf.exception.take() {
Err(exception.into())
} else {
Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
"No result or exception available. `get` can only be called once.",
))
}
}
}
pub fn convert_result_to_maybe_py(
result: Result<DiscoveryResult, Error>,
) -> PyResult<PyMaybeDiscoveryResult> {
match result {
Ok(result) => Ok(PyMaybeDiscoveryResult {
result: Some(convert_result_to_py(result)),
exception: None,
}),
Err(e) => Ok(PyMaybeDiscoveryResult {
result: None,
exception: Some(ErrorWrapper::from(e)),
}),
}
}
fn convert_result_to_py(result: DiscoveryResult) -> PyDiscoveryResult {
match result {
DiscoveryResult::GenericDevice {
device_info,
handler,
} => PyDiscoveryResult::GenericDevice {
device_info: *device_info,
handler: PyGenericDeviceHandler::new(handler),
},
DiscoveryResult::Light {
device_info,
handler,
} => PyDiscoveryResult::Light {
device_info: *device_info,
handler: PyLightHandler::new(handler),
},
DiscoveryResult::ColorLight {
device_info,
handler,
} => PyDiscoveryResult::ColorLight {
device_info: *device_info,
handler: PyColorLightHandler::new(handler),
},
DiscoveryResult::RgbLightStrip {
device_info,
handler,
} => PyDiscoveryResult::RgbLightStrip {
device_info: *device_info,
handler: PyRgbLightStripHandler::new(handler),
},
DiscoveryResult::RgbicLightStrip {
device_info,
handler,
} => PyDiscoveryResult::RgbicLightStrip {
device_info: *device_info,
handler: PyRgbicLightStripHandler::new(handler),
},
DiscoveryResult::Plug {
device_info,
handler,
} => PyDiscoveryResult::Plug {
device_info: *device_info,
handler: PyPlugHandler::new(handler),
},
DiscoveryResult::PlugEnergyMonitoring {
device_info,
handler,
} => PyDiscoveryResult::PlugEnergyMonitoring {
device_info: *device_info,
handler: PyPlugEnergyMonitoringHandler::new(handler),
},
DiscoveryResult::PowerStrip {
device_info,
handler,
} => PyDiscoveryResult::PowerStrip {
device_info: *device_info,
handler: PyPowerStripHandler::new(handler),
},
DiscoveryResult::PowerStripEnergyMonitoring {
device_info,
handler,
} => PyDiscoveryResult::PowerStripEnergyMonitoring {
device_info: *device_info,
handler: PyPowerStripEnergyMonitoringHandler::new(handler),
},
DiscoveryResult::Hub {
device_info,
handler,
} => PyDiscoveryResult::Hub {
device_info: *device_info,
handler: PyHubHandler::new(handler),
},
}
}

View File

@@ -0,0 +1,63 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use tapo::GenericDeviceHandler;
use tapo::responses::DeviceInfoGenericResult;
use tokio::sync::RwLock;
use crate::call_handler_method;
#[derive(Clone)]
#[pyclass(name = "GenericDeviceHandler")]
pub struct PyGenericDeviceHandler {
inner: Arc<RwLock<GenericDeviceHandler>>,
}
impl PyGenericDeviceHandler {
pub fn new(handler: GenericDeviceHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
}
#[pymethods]
impl PyGenericDeviceHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
GenericDeviceHandler::refresh_session,
discard_result
)
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), GenericDeviceHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), GenericDeviceHandler::off)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoGenericResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
GenericDeviceHandler::get_device_info
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
GenericDeviceHandler::get_device_info_json,
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
}

View File

@@ -0,0 +1,291 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use tapo::requests::{AlarmDuration, AlarmRingtone, AlarmVolume};
use tapo::responses::{ChildDeviceHubResult, DeviceInfoHubResult};
use tapo::{DeviceManagementExt as _, Error, HubDevice, HubHandler};
use tokio::sync::RwLock;
use crate::api::{
PyKE100Handler, PyS200BHandler, PyT31XHandler, PyT100Handler, PyT110Handler, PyT300Handler,
};
use crate::call_handler_method;
use crate::errors::ErrorWrapper;
use crate::requests::PyAlarmDuration;
#[derive(Clone)]
#[pyclass(name = "HubHandler")]
pub struct PyHubHandler {
inner: Arc<RwLock<HubHandler>>,
}
impl PyHubHandler {
pub fn new(handler: HubHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
fn parse_identifier(
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<HubDevice> {
match (device_id, nickname) {
(Some(device_id), _) => Ok(HubDevice::ByDeviceId(device_id)),
(None, Some(nickname)) => Ok(HubDevice::ByNickname(nickname)),
_ => Err(Into::<ErrorWrapper>::into(Error::Validation {
field: "identifier".to_string(),
message: "Either a device_id or nickname must be provided".to_string(),
})
.into()),
}
}
}
#[pymethods]
impl PyHubHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
HubHandler::refresh_session,
discard_result
)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
HubHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), HubHandler::device_reset)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoHubResult> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), HubHandler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
HubHandler::get_device_info_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_child_device_list(&self) -> PyResult<Py<PyList>> {
let handler = self.inner.clone();
let children = call_handler_method!(
handler.read().await.deref(),
HubHandler::get_child_device_list
)?;
Python::attach(|py| {
let results = PyList::empty(py);
for child in children {
match child {
ChildDeviceHubResult::KE100(device) => {
results.append(device.into_pyobject(py)?)?;
}
ChildDeviceHubResult::S200B(device) => {
results.append(device.into_pyobject(py)?)?;
}
ChildDeviceHubResult::T100(device) => {
results.append(device.into_pyobject(py)?)?;
}
ChildDeviceHubResult::T110(device) => {
results.append(device.into_pyobject(py)?)?;
}
ChildDeviceHubResult::T300(device) => {
results.append(device.into_pyobject(py)?)?;
}
ChildDeviceHubResult::T310(device) => {
results.append(device.into_pyobject(py)?)?;
}
ChildDeviceHubResult::T315(device) => {
results.append(device.into_pyobject(py)?)?;
}
_ => {
results.append(py.None())?;
}
}
}
Ok(results.into())
})
}
pub async fn get_child_device_list_json(&self, start_index: u64) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
HubHandler::get_child_device_list_json,
start_index
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_child_device_component_list_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
HubHandler::get_child_device_component_list_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_supported_ringtone_list(&self) -> PyResult<Vec<String>> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
HubHandler::get_supported_ringtone_list
)
}
#[pyo3(signature = (ringtone, volume, duration, seconds=None))]
pub async fn play_alarm(
&self,
ringtone: AlarmRingtone,
volume: AlarmVolume,
duration: PyAlarmDuration,
seconds: Option<u32>,
) -> PyResult<()> {
let handler = self.inner.clone();
let duration = match duration {
PyAlarmDuration::Continuous => AlarmDuration::Continuous,
PyAlarmDuration::Once => AlarmDuration::Once,
PyAlarmDuration::Seconds => {
if let Some(seconds) = seconds {
AlarmDuration::Seconds(seconds)
} else {
return Err(Into::<ErrorWrapper>::into(Error::Validation {
field: "seconds".to_string(),
message:
"A value must be provided for seconds when duration = AlarmDuration.Seconds"
.to_string(),
})
.into());
}
}
};
call_handler_method!(
handler.read().await.deref(),
HubHandler::play_alarm,
ringtone,
volume,
duration
)
}
pub async fn stop_alarm(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), HubHandler::stop_alarm)
}
#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn ke100(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyKE100Handler> {
let handler = self.inner.clone();
let identifier = PyHubHandler::parse_identifier(device_id, nickname)?;
let child_handler =
call_handler_method!(handler.read().await.deref(), HubHandler::ke100, identifier)?;
Ok(PyKE100Handler::new(child_handler))
}
#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn s200b(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyS200BHandler> {
let handler = self.inner.clone();
let identifier = PyHubHandler::parse_identifier(device_id, nickname)?;
let child_handler =
call_handler_method!(handler.read().await.deref(), HubHandler::s200b, identifier)?;
Ok(PyS200BHandler::new(child_handler))
}
#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn t100(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyT100Handler> {
let handler = self.inner.clone();
let identifier = PyHubHandler::parse_identifier(device_id, nickname)?;
let child_handler =
call_handler_method!(handler.read().await.deref(), HubHandler::t100, identifier)?;
Ok(PyT100Handler::new(child_handler))
}
#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn t110(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyT110Handler> {
let handler = self.inner.clone();
let identifier = PyHubHandler::parse_identifier(device_id, nickname)?;
let child_handler =
call_handler_method!(handler.read().await.deref(), HubHandler::t110, identifier)?;
Ok(PyT110Handler::new(child_handler))
}
#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn t300(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyT300Handler> {
let handler = self.inner.clone();
let identifier = PyHubHandler::parse_identifier(device_id, nickname)?;
let child_handler =
call_handler_method!(handler.read().await.deref(), HubHandler::t300, identifier)?;
Ok(PyT300Handler::new(child_handler))
}
#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn t310(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyT31XHandler> {
let handler = self.inner.clone();
let identifier = PyHubHandler::parse_identifier(device_id, nickname)?;
let child_handler =
call_handler_method!(handler.read().await.deref(), HubHandler::t310, identifier)?;
Ok(PyT31XHandler::new(child_handler))
}
#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn t315(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyT31XHandler> {
self.t310(device_id, nickname).await
}
}

View File

@@ -0,0 +1,88 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use tapo::responses::{DeviceInfoLightResult, DeviceUsageEnergyMonitoringResult};
use tapo::{DeviceManagementExt as _, LightHandler};
use tokio::sync::RwLock;
use crate::call_handler_method;
#[derive(Clone)]
#[pyclass(name = "LightHandler")]
pub struct PyLightHandler {
inner: Arc<RwLock<LightHandler>>,
}
impl PyLightHandler {
pub fn new(handler: LightHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
}
#[pymethods]
impl PyLightHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
LightHandler::refresh_session,
discard_result
)
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), LightHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), LightHandler::off)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
LightHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), LightHandler::device_reset)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoLightResult> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), LightHandler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
LightHandler::get_device_info_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_device_usage(&self) -> PyResult<DeviceUsageEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), LightHandler::get_device_usage)
}
pub async fn set_brightness(&self, brightness: u8) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
LightHandler::set_brightness,
brightness
)
}
}

View File

@@ -0,0 +1,167 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use chrono::{DateTime, NaiveDate, Utc};
use pyo3::prelude::*;
use pyo3::types::PyDict;
use tapo::requests::{EnergyDataInterval, PowerDataInterval};
use tapo::responses::{
CurrentPowerResult, DeviceInfoPlugEnergyMonitoringResult, DeviceUsageEnergyMonitoringResult,
EnergyDataResult, EnergyUsageResult, PowerDataResult,
};
use tapo::{DeviceManagementExt as _, PlugEnergyMonitoringHandler};
use tokio::sync::RwLock;
use crate::call_handler_method;
use crate::requests::{PyEnergyDataInterval, PyPowerDataInterval};
#[derive(Clone)]
#[pyclass(name = "PlugEnergyMonitoringHandler")]
pub struct PyPlugEnergyMonitoringHandler {
inner: Arc<RwLock<PlugEnergyMonitoringHandler>>,
}
impl PyPlugEnergyMonitoringHandler {
pub fn new(handler: PlugEnergyMonitoringHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
}
#[pymethods]
impl PyPlugEnergyMonitoringHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
PlugEnergyMonitoringHandler::refresh_session,
discard_result
)
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::on
)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::off
)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::device_reset,
)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoPlugEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::get_device_info,
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::get_device_info_json,
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_current_power(&self) -> PyResult<CurrentPowerResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::get_current_power,
)
}
pub async fn get_device_usage(&self) -> PyResult<DeviceUsageEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::get_device_usage,
)
}
pub async fn get_energy_usage(&self) -> PyResult<EnergyUsageResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::get_energy_usage,
)
}
#[pyo3(signature = (interval, start_date, end_date=None))]
pub async fn get_energy_data(
&self,
interval: PyEnergyDataInterval,
start_date: NaiveDate,
end_date: Option<NaiveDate>,
) -> PyResult<EnergyDataResult> {
let interval = match interval {
PyEnergyDataInterval::Hourly => EnergyDataInterval::Hourly {
start_date,
end_date: end_date.unwrap_or(start_date),
},
PyEnergyDataInterval::Daily => EnergyDataInterval::Daily { start_date },
PyEnergyDataInterval::Monthly => EnergyDataInterval::Monthly { start_date },
};
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::get_energy_data,
interval
)?;
Ok(result)
}
pub async fn get_power_data(
&self,
interval: PyPowerDataInterval,
start_date_time: DateTime<Utc>,
end_date_time: DateTime<Utc>,
) -> PyResult<PowerDataResult> {
let interval = match interval {
PyPowerDataInterval::Every5Minutes => PowerDataInterval::Every5Minutes {
start_date_time,
end_date_time,
},
PyPowerDataInterval::Hourly => PowerDataInterval::Hourly {
start_date_time,
end_date_time,
},
};
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PlugEnergyMonitoringHandler::get_power_data,
interval
)?;
Ok(result)
}
}

View File

@@ -0,0 +1,79 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use tapo::responses::{DeviceInfoPlugResult, DeviceUsageResult};
use tapo::{DeviceManagementExt as _, PlugHandler};
use tokio::sync::RwLock;
use crate::call_handler_method;
#[derive(Clone)]
#[pyclass(name = "PlugHandler")]
pub struct PyPlugHandler {
inner: Arc<RwLock<PlugHandler>>,
}
impl PyPlugHandler {
pub fn new(handler: PlugHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
}
#[pymethods]
impl PyPlugHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
PlugHandler::refresh_session,
discard_result
)
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), PlugHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), PlugHandler::off)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PlugHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), PlugHandler::device_reset)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoPlugResult> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), PlugHandler::get_device_info)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PlugHandler::get_device_info_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_device_usage(&self) -> PyResult<DeviceUsageResult> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), PlugHandler::get_device_usage)
}
}

View File

@@ -0,0 +1,144 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use tapo::responses::DeviceInfoPowerStripResult;
use tapo::{DeviceManagementExt as _, Error, Plug, PowerStripEnergyMonitoringHandler};
use tokio::sync::RwLock;
use crate::api::PyPowerStripPlugEnergyMonitoringHandler;
use crate::call_handler_method;
use crate::errors::ErrorWrapper;
#[derive(Clone)]
#[pyclass(name = "PowerStripEnergyMonitoringHandler")]
pub struct PyPowerStripEnergyMonitoringHandler {
inner: Arc<RwLock<PowerStripEnergyMonitoringHandler>>,
}
impl PyPowerStripEnergyMonitoringHandler {
pub fn new(handler: PowerStripEnergyMonitoringHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
fn parse_identifier(
device_id: Option<String>,
nickname: Option<String>,
position: Option<u8>,
) -> PyResult<Plug> {
match (device_id, nickname, position) {
(Some(device_id), _, _) => Ok(Plug::ByDeviceId(device_id)),
(None, Some(nickname), _) => Ok(Plug::ByNickname(nickname)),
(None, None, Some(position)) => Ok(Plug::ByPosition(position)),
_ => Err(Into::<ErrorWrapper>::into(Error::Validation {
field: "identifier".to_string(),
message: "Either a device_id, nickname, or position must be provided".to_string(),
})
.into()),
}
}
}
#[pymethods]
impl PyPowerStripEnergyMonitoringHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
PowerStripEnergyMonitoringHandler::refresh_session,
discard_result
)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::device_reset,
)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoPowerStripResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::get_device_info
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::get_device_info_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_child_device_list(&self) -> PyResult<Py<PyList>> {
let handler = self.inner.clone();
let children = call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::get_child_device_list
)?;
Python::attach(|py| {
let results = PyList::empty(py);
for child in children {
results.append(child.into_pyobject(py)?)?;
}
Ok(results.into())
})
}
pub async fn get_child_device_list_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::get_child_device_list_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_child_device_component_list_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::get_child_device_component_list_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
#[pyo3(signature = (device_id=None, nickname=None, position=None))]
pub async fn plug(
&self,
device_id: Option<String>,
nickname: Option<String>,
position: Option<u8>,
) -> PyResult<PyPowerStripPlugEnergyMonitoringHandler> {
let handler = self.inner.clone();
let identifier =
PyPowerStripEnergyMonitoringHandler::parse_identifier(device_id, nickname, position)?;
let child_handler = call_handler_method!(
handler.read().await.deref(),
PowerStripEnergyMonitoringHandler::plug,
identifier
)?;
Ok(PyPowerStripPlugEnergyMonitoringHandler::new(child_handler))
}
}

View File

@@ -0,0 +1,143 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList};
use tapo::responses::DeviceInfoPowerStripResult;
use tapo::{DeviceManagementExt as _, Error, Plug, PowerStripHandler};
use tokio::sync::RwLock;
use crate::api::PyPowerStripPlugHandler;
use crate::call_handler_method;
use crate::errors::ErrorWrapper;
#[derive(Clone)]
#[pyclass(name = "PowerStripHandler")]
pub struct PyPowerStripHandler {
inner: Arc<RwLock<PowerStripHandler>>,
}
impl PyPowerStripHandler {
pub fn new(handler: PowerStripHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
fn parse_identifier(
device_id: Option<String>,
nickname: Option<String>,
position: Option<u8>,
) -> PyResult<Plug> {
match (device_id, nickname, position) {
(Some(device_id), _, _) => Ok(Plug::ByDeviceId(device_id)),
(None, Some(nickname), _) => Ok(Plug::ByNickname(nickname)),
(None, None, Some(position)) => Ok(Plug::ByPosition(position)),
_ => Err(Into::<ErrorWrapper>::into(Error::Validation {
field: "identifier".to_string(),
message: "Either a device_id, nickname, or position must be provided".to_string(),
})
.into()),
}
}
}
#[pymethods]
impl PyPowerStripHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
PowerStripHandler::refresh_session,
discard_result
)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::device_reset,
)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoPowerStripResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::get_device_info
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::get_device_info_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_child_device_list(&self) -> PyResult<Py<PyList>> {
let handler = self.inner.clone();
let children = call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::get_child_device_list
)?;
Python::attach(|py| {
let results = PyList::empty(py);
for child in children {
results.append(child.into_pyobject(py)?)?;
}
Ok(results.into())
})
}
pub async fn get_child_device_list_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::get_child_device_list_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_child_device_component_list_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::get_child_device_component_list_json
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
#[pyo3(signature = (device_id=None, nickname=None, position=None))]
pub async fn plug(
&self,
device_id: Option<String>,
nickname: Option<String>,
position: Option<u8>,
) -> PyResult<PyPowerStripPlugHandler> {
let handler = self.inner.clone();
let identifier = PyPowerStripHandler::parse_identifier(device_id, nickname, position)?;
let child_handler = call_handler_method!(
handler.read().await.deref(),
PowerStripHandler::plug,
identifier
)?;
Ok(PyPowerStripPlugHandler::new(child_handler))
}
}

View File

@@ -0,0 +1,8 @@
use std::sync::Arc;
use tapo::HandlerExt;
use tokio::sync::RwLock;
pub trait PyHandlerExt: Send + Sync + Sized {
fn get_inner_handler(&self) -> Arc<RwLock<impl HandlerExt + 'static>>;
}

View File

@@ -0,0 +1,138 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use tapo::requests::Color;
use tapo::responses::{DeviceInfoRgbLightStripResult, DeviceUsageEnergyMonitoringResult};
use tapo::{DeviceManagementExt as _, HandlerExt, RgbLightStripHandler};
use tokio::sync::RwLock;
use crate::api::PyHandlerExt;
use crate::call_handler_method;
use crate::requests::PyColorLightSetDeviceInfoParams;
#[derive(Clone)]
#[pyclass(name = "RgbLightStripHandler")]
pub struct PyRgbLightStripHandler {
pub inner: Arc<RwLock<RgbLightStripHandler>>,
}
impl PyRgbLightStripHandler {
pub fn new(handler: RgbLightStripHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
}
impl PyHandlerExt for PyRgbLightStripHandler {
fn get_inner_handler(&self) -> Arc<RwLock<impl HandlerExt + 'static>> {
Arc::clone(&self.inner)
}
}
#[pymethods]
impl PyRgbLightStripHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
RgbLightStripHandler::refresh_session,
discard_result
)
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), RgbLightStripHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), RgbLightStripHandler::off)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::device_reset,
)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoRgbLightStripResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::get_device_info
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::get_device_info_json,
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_device_usage(&self) -> PyResult<DeviceUsageEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::get_device_usage
)
}
pub fn set(&self) -> PyColorLightSetDeviceInfoParams {
PyColorLightSetDeviceInfoParams::new()
}
pub async fn set_brightness(&self, brightness: u8) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::set_brightness,
brightness
)
}
pub async fn set_color(&self, color: Color) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::set_color,
color
)
}
pub async fn set_hue_saturation(&self, hue: u16, saturation: u8) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::set_hue_saturation,
hue,
saturation
)
}
pub async fn set_color_temperature(&self, color_temperature: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbLightStripHandler::set_color_temperature,
color_temperature
)
}
}

View File

@@ -0,0 +1,167 @@
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use tapo::requests::{Color, LightingEffect, LightingEffectPreset};
use tapo::responses::{DeviceInfoRgbicLightStripResult, DeviceUsageEnergyMonitoringResult};
use tapo::{DeviceManagementExt as _, HandlerExt, RgbicLightStripHandler};
use tokio::sync::RwLock;
use crate::api::PyHandlerExt;
use crate::call_handler_method;
use crate::requests::{PyColorLightSetDeviceInfoParams, PyLightingEffect};
#[derive(Clone)]
#[pyclass(name = "RgbicLightStripHandler")]
pub struct PyRgbicLightStripHandler {
pub inner: Arc<RwLock<RgbicLightStripHandler>>,
}
impl PyRgbicLightStripHandler {
pub fn new(handler: RgbicLightStripHandler) -> Self {
Self {
inner: Arc::new(RwLock::new(handler)),
}
}
}
impl PyHandlerExt for PyRgbicLightStripHandler {
fn get_inner_handler(&self) -> Arc<RwLock<impl HandlerExt + 'static>> {
Arc::clone(&self.inner)
}
}
#[pymethods]
impl PyRgbicLightStripHandler {
pub async fn refresh_session(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.write().await.deref_mut(),
RgbicLightStripHandler::refresh_session,
discard_result
)
}
pub async fn on(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), RgbicLightStripHandler::on)
}
pub async fn off(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(handler.read().await.deref(), RgbicLightStripHandler::off)
}
pub async fn device_reboot(&self, delay_s: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::device_reboot,
delay_s
)
}
pub async fn device_reset(&self) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::device_reset,
)
}
pub async fn get_device_info(&self) -> PyResult<DeviceInfoRgbicLightStripResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::get_device_info
)
}
pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.inner.clone();
let result = call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::get_device_info_json,
)?;
Python::attach(|py| tapo::python::serde_object_to_py_dict(py, &result))
}
pub async fn get_device_usage(&self) -> PyResult<DeviceUsageEnergyMonitoringResult> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::get_device_usage
)
}
pub fn set(&self) -> PyColorLightSetDeviceInfoParams {
PyColorLightSetDeviceInfoParams::new()
}
pub async fn set_brightness(&self, brightness: u8) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::set_brightness,
brightness
)
}
pub async fn set_color(&self, color: Color) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::set_color,
color
)
}
pub async fn set_hue_saturation(&self, hue: u16, saturation: u8) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::set_hue_saturation,
hue,
saturation
)
}
pub async fn set_color_temperature(&self, color_temperature: u16) -> PyResult<()> {
let handler = self.inner.clone();
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::set_color_temperature,
color_temperature
)
}
pub async fn set_lighting_effect(&self, lighting_effect: Py<PyAny>) -> PyResult<()> {
let handler = self.inner.clone();
let lighting_effect = map_lighting_effect(lighting_effect)?;
call_handler_method!(
handler.read().await.deref(),
RgbicLightStripHandler::set_lighting_effect,
lighting_effect
)
}
}
fn map_lighting_effect(lighting_effect: Py<PyAny>) -> PyResult<LightingEffect> {
if let Some(lighting_effect) =
Python::attach(|py| lighting_effect.extract::<LightingEffectPreset>(py).ok())
{
return Ok(lighting_effect.into());
}
if let Some(lighting_effect) =
Python::attach(|py| lighting_effect.extract::<PyLightingEffect>(py).ok())
{
return Ok(lighting_effect.into());
}
Err(PyErr::new::<PyTypeError, _>(
"Invalid lighting effect type. Must be one of `LightingEffect` or `LightingEffectPreset`",
))
}

View File

@@ -0,0 +1,23 @@
use pyo3::PyErr;
use pyo3::exceptions::PyException;
use tapo::Error;
pub struct ErrorWrapper(pub Error);
impl From<Error> for ErrorWrapper {
fn from(err: Error) -> Self {
Self(err)
}
}
impl From<anyhow::Error> for ErrorWrapper {
fn from(err: anyhow::Error) -> Self {
Self(err.into())
}
}
impl From<ErrorWrapper> for PyErr {
fn from(err: ErrorWrapper) -> PyErr {
PyException::new_err(format!("{:?}", err.0))
}
}

View File

@@ -0,0 +1,209 @@
mod api;
mod errors;
mod requests;
mod responses;
mod runtime;
use log::LevelFilter;
use pyo3::prelude::*;
use pyo3_log::{Caching, Logger};
use tapo::requests::{AlarmRingtone, AlarmVolume, Color, LightingEffectPreset, LightingEffectType};
use tapo::responses::{
AutoOffStatus, ColorLightState, CurrentPowerResult, DefaultBrightnessState,
DefaultColorLightState, DefaultLightState, DefaultPlugState, DefaultPowerType,
DefaultRgbLightStripState, DefaultRgbicLightStripState, DefaultStateType,
DeviceInfoColorLightResult, DeviceInfoGenericResult, DeviceInfoHubResult,
DeviceInfoLightResult, DeviceInfoPlugEnergyMonitoringResult, DeviceInfoPlugResult,
DeviceInfoPowerStripResult, DeviceInfoRgbLightStripResult, DeviceInfoRgbicLightStripResult,
DeviceUsageEnergyMonitoringResult, DeviceUsageResult, EnergyDataIntervalResult,
EnergyDataResult, EnergyUsageResult, KE100Result, OvercurrentStatus, OverheatStatus, PlugState,
PowerDataIntervalResult, PowerDataResult, PowerProtectionStatus,
PowerStripPlugEnergyMonitoringResult, PowerStripPlugResult, RgbLightStripState,
RgbicLightStripState, S200BLog, S200BResult, S200BRotationParams, Status, T31XResult, T100Log,
T100Result, T110Log, T110Result, T300Log, T300Result, TemperatureHumidityRecord,
TemperatureHumidityRecords, TemperatureUnit, TemperatureUnitKE100, UsageByPeriodResult,
WaterLeakStatus,
};
use api::{
PyApiClient, PyColorLightHandler, PyDeviceDiscovery, PyDeviceDiscoveryIter, PyDiscoveryResult,
PyGenericDeviceHandler, PyHubHandler, PyKE100Handler, PyLightHandler, PyMaybeDiscoveryResult,
PyPlugEnergyMonitoringHandler, PyPlugHandler, PyPowerStripEnergyMonitoringHandler,
PyPowerStripHandler, PyPowerStripPlugEnergyMonitoringHandler, PyPowerStripPlugHandler,
PyRgbLightStripHandler, PyRgbicLightStripHandler, PyT31XHandler, PyT100Handler, PyT110Handler,
PyT300Handler,
};
use requests::{
PyAlarmDuration, PyColorLightSetDeviceInfoParams, PyEnergyDataInterval, PyLightingEffect,
PyPowerDataInterval,
};
use responses::{
TriggerLogsS200BResult, TriggerLogsT100Result, TriggerLogsT110Result, TriggerLogsT300Result,
};
#[pymodule]
#[pyo3(name = "tapo")]
fn tapo_py(py: Python, module: &Bound<'_, PyModule>) -> PyResult<()> {
Logger::new(py, Caching::LoggersAndLevels)?
.filter(LevelFilter::Trace)
.install()
.expect("Failed to install the logger");
let requests = PyModule::new(py, "tapo.requests")?;
let responses = PyModule::new(py, "tapo.responses")?;
register_handlers(module)?;
register_requests(&requests)?;
register_responses(&responses)?;
register_responses_hub(&responses)?;
register_responses_power_strip(&responses)?;
module.add_submodule(&requests)?;
module.add_submodule(&responses)?;
let sys = py.import("sys")?;
let modules = sys.getattr("modules")?;
modules.set_item("tapo.requests", requests)?;
modules.set_item("tapo.responses", responses)?;
Ok(())
}
fn register_requests(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<Color>()?;
module.add_class::<PyLightingEffect>()?;
module.add_class::<LightingEffectPreset>()?;
module.add_class::<LightingEffectType>()?;
module.add_class::<PyColorLightSetDeviceInfoParams>()?;
module.add_class::<PyEnergyDataInterval>()?;
module.add_class::<PyPowerDataInterval>()?;
// hub requests
module.add_class::<AlarmRingtone>()?;
module.add_class::<AlarmVolume>()?;
module.add_class::<PyAlarmDuration>()?;
module.add_class::<TemperatureUnitKE100>()?;
Ok(())
}
fn register_handlers(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<PyApiClient>()?;
module.add_class::<PyColorLightHandler>()?;
module.add_class::<PyGenericDeviceHandler>()?;
module.add_class::<PyLightHandler>()?;
module.add_class::<PyPlugEnergyMonitoringHandler>()?;
module.add_class::<PyPlugHandler>()?;
module.add_class::<PyRgbLightStripHandler>()?;
module.add_class::<PyRgbicLightStripHandler>()?;
module.add_class::<PyHubHandler>()?;
module.add_class::<PyKE100Handler>()?;
module.add_class::<PyT100Handler>()?;
module.add_class::<PyT110Handler>()?;
module.add_class::<PyT300Handler>()?;
module.add_class::<PyT31XHandler>()?;
module.add_class::<PyPowerStripHandler>()?;
module.add_class::<PyPowerStripEnergyMonitoringHandler>()?;
module.add_class::<PyPowerStripPlugHandler>()?;
module.add_class::<PyPowerStripPlugEnergyMonitoringHandler>()?;
module.add_class::<PyDeviceDiscovery>()?;
module.add_class::<PyDeviceDiscoveryIter>()?;
module.add_class::<PyDiscoveryResult>()?;
module.add_class::<PyMaybeDiscoveryResult>()?;
Ok(())
}
fn register_responses(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<CurrentPowerResult>()?;
module.add_class::<DefaultBrightnessState>()?;
module.add_class::<DefaultPowerType>()?;
module.add_class::<DefaultStateType>()?;
module.add_class::<DeviceUsageEnergyMonitoringResult>()?;
module.add_class::<DeviceUsageResult>()?;
module.add_class::<EnergyDataIntervalResult>()?;
module.add_class::<EnergyDataResult>()?;
module.add_class::<EnergyUsageResult>()?;
module.add_class::<OvercurrentStatus>()?;
module.add_class::<OverheatStatus>()?;
module.add_class::<PowerDataIntervalResult>()?;
module.add_class::<PowerDataResult>()?;
module.add_class::<PowerProtectionStatus>()?;
module.add_class::<UsageByPeriodResult>()?;
// device info: generic
module.add_class::<DeviceInfoGenericResult>()?;
// device info: light
module.add_class::<DeviceInfoLightResult>()?;
module.add_class::<DefaultLightState>()?;
// device info: color light
module.add_class::<DeviceInfoColorLightResult>()?;
module.add_class::<DefaultColorLightState>()?;
module.add_class::<ColorLightState>()?;
// device info: rgb light strip
module.add_class::<DeviceInfoRgbLightStripResult>()?;
module.add_class::<DefaultRgbLightStripState>()?;
module.add_class::<RgbLightStripState>()?;
// device info: rgbic light strip
module.add_class::<DeviceInfoRgbicLightStripResult>()?;
module.add_class::<DefaultRgbicLightStripState>()?;
module.add_class::<RgbicLightStripState>()?;
module.add_class::<PyLightingEffect>()?;
module.add_class::<LightingEffectType>()?;
// device info: plugs
module.add_class::<DefaultPlugState>()?;
module.add_class::<DeviceInfoPlugEnergyMonitoringResult>()?;
module.add_class::<DeviceInfoPlugResult>()?;
module.add_class::<PlugState>()?;
Ok(())
}
fn register_responses_hub(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<DeviceInfoHubResult>()?;
module.add_class::<KE100Result>()?;
module.add_class::<S200BResult>()?;
module.add_class::<T100Result>()?;
module.add_class::<T110Result>()?;
module.add_class::<T300Result>()?;
module.add_class::<T31XResult>()?;
// child devices
module.add_class::<S200BLog>()?;
module.add_class::<S200BRotationParams>()?;
module.add_class::<Status>()?;
module.add_class::<T100Log>()?;
module.add_class::<T110Log>()?;
module.add_class::<T300Log>()?;
module.add_class::<TemperatureHumidityRecord>()?;
module.add_class::<TemperatureHumidityRecords>()?;
module.add_class::<TemperatureUnit>()?;
module.add_class::<TemperatureUnitKE100>()?;
module.add_class::<TriggerLogsS200BResult>()?;
module.add_class::<TriggerLogsT100Result>()?;
module.add_class::<TriggerLogsT110Result>()?;
module.add_class::<TriggerLogsT300Result>()?;
module.add_class::<WaterLeakStatus>()?;
Ok(())
}
fn register_responses_power_strip(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<DeviceInfoPowerStripResult>()?;
// child devices
module.add_class::<AutoOffStatus>()?;
module.add_class::<PowerStripPlugResult>()?;
module.add_class::<PowerStripPlugEnergyMonitoringResult>()?;
Ok(())
}

View File

@@ -0,0 +1,9 @@
mod energy_data_interval;
mod play_alarm;
mod power_data_interval;
mod set_device_info;
pub use energy_data_interval::*;
pub use play_alarm::*;
pub use power_data_interval::*;
pub use set_device_info::*;

View File

@@ -0,0 +1,9 @@
use pyo3::prelude::*;
#[derive(Clone, PartialEq)]
#[pyclass(name = "EnergyDataInterval", eq, eq_int)]
pub enum PyEnergyDataInterval {
Hourly,
Daily,
Monthly,
}

View File

@@ -0,0 +1,9 @@
use pyo3::prelude::*;
#[derive(Debug, Clone, PartialEq)]
#[pyclass(name = "AlarmDuration", eq)]
pub enum PyAlarmDuration {
Continuous,
Once,
Seconds,
}

View File

@@ -0,0 +1,8 @@
use pyo3::prelude::*;
#[derive(Clone, PartialEq)]
#[pyclass(name = "PowerDataInterval", eq, eq_int)]
pub enum PyPowerDataInterval {
Every5Minutes,
Hourly,
}

View File

@@ -0,0 +1,5 @@
mod color_light;
mod lighting_effect;
pub use color_light::*;
pub use lighting_effect::*;

View File

@@ -0,0 +1,108 @@
use std::ops::Deref;
use pyo3::prelude::*;
use tapo::requests::{Color, ColorLightSetDeviceInfoParams};
use crate::api::{
PyColorLightHandler, PyHandlerExt, PyRgbLightStripHandler, PyRgbicLightStripHandler,
};
use crate::errors::ErrorWrapper;
use crate::runtime::tokio;
#[derive(Clone)]
#[pyclass(name = "LightSetDeviceInfoParams")]
pub struct PyColorLightSetDeviceInfoParams {
params: ColorLightSetDeviceInfoParams,
}
impl PyColorLightSetDeviceInfoParams {
pub(crate) fn new() -> Self {
Self {
params: ColorLightSetDeviceInfoParams::new(),
}
}
async fn _send_to_inner_handler(&self, handler: impl PyHandlerExt) -> PyResult<()> {
let params = self.params.clone();
let handler = handler.get_inner_handler();
tokio()
.spawn(async move {
let handler_lock = handler.read().await;
params
.send(handler_lock.deref())
.await
.map_err(ErrorWrapper)?;
Ok::<_, ErrorWrapper>(())
})
.await
.map_err(anyhow::Error::from)
.map_err(ErrorWrapper::from)??;
Ok(())
}
}
#[pymethods]
impl PyColorLightSetDeviceInfoParams {
pub fn on(&self) -> Self {
Self {
params: self.params.clone().on(),
}
}
pub fn off(&self) -> Self {
Self {
params: self.params.clone().off(),
}
}
pub fn brightness(&self, brightness: u8) -> Self {
Self {
params: self.params.clone().brightness(brightness),
}
}
pub fn color(&self, color: Color) -> Self {
Self {
params: self.params.clone().color(color),
}
}
pub fn hue_saturation(&self, hue: u16, saturation: u8) -> Self {
Self {
params: self.params.clone().hue_saturation(hue, saturation),
}
}
pub fn color_temperature(&self, color_temperature: u16) -> Self {
Self {
params: self.params.clone().color_temperature(color_temperature),
}
}
async fn send(&self, handler: Py<PyAny>) -> PyResult<()> {
if let Some(handler) = Python::attach(|py| handler.extract::<PyColorLightHandler>(py).ok())
{
return self._send_to_inner_handler(handler).await;
}
if let Some(handler) =
Python::attach(|py| handler.extract::<PyRgbLightStripHandler>(py).ok())
{
return self._send_to_inner_handler(handler).await;
}
if let Some(handler) =
Python::attach(|py| handler.extract::<PyRgbicLightStripHandler>(py).ok())
{
return self._send_to_inner_handler(handler).await;
}
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
"Invalid handler type. Must be one of `PyColorLightHandler`, `PyRgbLightStripHandler` or `PyRgbicLightStripHandler`",
))
}
}

View File

@@ -0,0 +1,201 @@
use pyo3::prelude::*;
use tapo::requests::{LightingEffect, LightingEffectType};
#[derive(Clone)]
#[pyclass(name = "LightingEffect")]
pub struct PyLightingEffect {
inner: LightingEffect,
}
#[pymethods]
impl PyLightingEffect {
#[new]
fn new(
name: String,
r#type: LightingEffectType,
is_custom: bool,
enabled: bool,
brightness: u8,
display_colors: Vec<[u16; 3]>,
) -> Self {
Self {
inner: LightingEffect::new(
name,
r#type,
is_custom,
enabled,
brightness,
display_colors,
),
}
}
pub fn with_brightness(mut slf: PyRefMut<'_, Self>, brightness: u8) -> PyRefMut<'_, Self> {
(*slf).inner.brightness = brightness;
slf
}
pub fn with_is_custom(mut slf: PyRefMut<'_, Self>, is_custom: bool) -> PyRefMut<'_, Self> {
(*slf).inner.is_custom = is_custom;
slf
}
pub fn with_display_colors(
mut slf: PyRefMut<'_, Self>,
display_colors: Vec<[u16; 3]>,
) -> PyRefMut<'_, Self> {
(*slf).inner.display_colors = display_colors;
slf
}
pub fn with_enabled(mut slf: PyRefMut<'_, Self>, enabled: bool) -> PyRefMut<'_, Self> {
(*slf).inner.enabled = enabled;
slf
}
pub fn with_id(mut slf: PyRefMut<'_, Self>, id: String) -> PyRefMut<'_, Self> {
(*slf).inner.id = id;
slf
}
pub fn with_name(mut slf: PyRefMut<'_, Self>, name: String) -> PyRefMut<'_, Self> {
(*slf).inner.name = name;
slf
}
pub fn with_type(
mut slf: PyRefMut<'_, Self>,
r#type: LightingEffectType,
) -> PyRefMut<'_, Self> {
(*slf).inner.r#type = r#type;
slf
}
pub fn with_backgrounds(
mut slf: PyRefMut<'_, Self>,
backgrounds: Vec<[u16; 3]>,
) -> PyRefMut<'_, Self> {
(*slf).inner.backgrounds = Some(backgrounds);
slf
}
pub fn with_brightness_range(
mut slf: PyRefMut<'_, Self>,
brightness_range: [u8; 2],
) -> PyRefMut<'_, Self> {
(*slf).inner.brightness_range = Some(brightness_range.to_vec());
slf
}
pub fn with_direction(mut slf: PyRefMut<'_, Self>, direction: u8) -> PyRefMut<'_, Self> {
(*slf).inner.direction = Some(direction);
slf
}
pub fn with_duration(mut slf: PyRefMut<'_, Self>, duration: u64) -> PyRefMut<'_, Self> {
(*slf).inner.duration = Some(duration);
slf
}
pub fn with_expansion_strategy(
mut slf: PyRefMut<'_, Self>,
expansion_strategy: u8,
) -> PyRefMut<'_, Self> {
(*slf).inner.expansion_strategy = Some(expansion_strategy);
slf
}
pub fn with_fade_off(mut slf: PyRefMut<'_, Self>, fade_off: u16) -> PyRefMut<'_, Self> {
(*slf).inner.fade_off = Some(fade_off);
slf
}
pub fn with_hue_range(mut slf: PyRefMut<'_, Self>, hue_range: [u16; 2]) -> PyRefMut<'_, Self> {
(*slf).inner.hue_range = Some(hue_range);
slf
}
pub fn with_init_states(
mut slf: PyRefMut<'_, Self>,
init_states: Vec<[u16; 3]>,
) -> PyRefMut<'_, Self> {
(*slf).inner.init_states = Some(init_states);
slf
}
pub fn with_random_seed(mut slf: PyRefMut<'_, Self>, random_seed: u64) -> PyRefMut<'_, Self> {
(*slf).inner.random_seed = Some(random_seed);
slf
}
pub fn with_repeat_times(mut slf: PyRefMut<'_, Self>, repeat_times: u8) -> PyRefMut<'_, Self> {
(*slf).inner.repeat_times = Some(repeat_times);
slf
}
pub fn with_run_time(mut slf: PyRefMut<'_, Self>, run_time: u64) -> PyRefMut<'_, Self> {
(*slf).inner.run_time = Some(run_time);
slf
}
pub fn with_saturation_range(
mut slf: PyRefMut<'_, Self>,
saturation_range: [u8; 2],
) -> PyRefMut<'_, Self> {
(*slf).inner.saturation_range = Some(saturation_range);
slf
}
pub fn with_segment_length(
mut slf: PyRefMut<'_, Self>,
segment_length: u8,
) -> PyRefMut<'_, Self> {
(*slf).inner.segment_length = Some(segment_length);
slf
}
pub fn with_segments(mut slf: PyRefMut<'_, Self>, segments: Vec<u8>) -> PyRefMut<'_, Self> {
(*slf).inner.segments = Some(segments);
slf
}
pub fn with_sequence(
mut slf: PyRefMut<'_, Self>,
sequence: Vec<[u16; 3]>,
) -> PyRefMut<'_, Self> {
(*slf).inner.sequence = Some(sequence);
slf
}
pub fn with_spread(mut slf: PyRefMut<'_, Self>, spread: u8) -> PyRefMut<'_, Self> {
(*slf).inner.spread = Some(spread);
slf
}
pub fn with_transition(mut slf: PyRefMut<'_, Self>, transition: u16) -> PyRefMut<'_, Self> {
(*slf).inner.transition = Some(transition);
slf
}
pub fn with_transition_range(
mut slf: PyRefMut<'_, Self>,
transition_range: [u16; 2],
) -> PyRefMut<'_, Self> {
(*slf).inner.transition_range = Some(transition_range);
slf
}
pub fn with_transition_sequence(
mut slf: PyRefMut<'_, Self>,
transition_sequence: Vec<u16>,
) -> PyRefMut<'_, Self> {
(*slf).inner.transition_sequence = Some(transition_sequence);
slf
}
}
impl From<PyLightingEffect> for LightingEffect {
fn from(effect: PyLightingEffect) -> Self {
effect.inner
}
}

View File

@@ -0,0 +1,3 @@
mod child_device_list_hub_result;
pub use child_device_list_hub_result::*;

View File

@@ -0,0 +1,9 @@
mod s200b_result;
mod t100_result;
mod t110_result;
mod t300_result;
pub use s200b_result::*;
pub use t100_result::*;
pub use t110_result::*;
pub use t300_result::*;

View File

@@ -0,0 +1,32 @@
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use tapo::responses::{S200BLog, TriggerLogsResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[pyclass(get_all)]
#[allow(missing_docs)]
pub struct TriggerLogsS200BResult {
start_id: u64,
sum: u64,
logs: Vec<S200BLog>,
}
impl From<TriggerLogsResult<S200BLog>> for TriggerLogsS200BResult {
fn from(result: TriggerLogsResult<S200BLog>) -> Self {
Self {
start_id: result.start_id,
sum: result.sum,
logs: result.logs,
}
}
}
#[pyo3::pymethods]
impl TriggerLogsS200BResult {
pub fn to_dict(&self, py: pyo3::Python) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
let value = serde_json::to_value(self)
.map_err(|e| pyo3::exceptions::PyException::new_err(e.to_string()))?;
tapo::python::serde_object_to_py_dict(py, &value)
}
}

View File

@@ -0,0 +1,32 @@
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use tapo::responses::{T100Log, TriggerLogsResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[pyclass(get_all)]
#[allow(missing_docs)]
pub struct TriggerLogsT100Result {
start_id: u64,
sum: u64,
logs: Vec<T100Log>,
}
impl From<TriggerLogsResult<T100Log>> for TriggerLogsT100Result {
fn from(result: TriggerLogsResult<T100Log>) -> Self {
Self {
start_id: result.start_id,
sum: result.sum,
logs: result.logs,
}
}
}
#[pyo3::pymethods]
impl TriggerLogsT100Result {
pub fn to_dict(&self, py: pyo3::Python) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
let value = serde_json::to_value(self)
.map_err(|e| pyo3::exceptions::PyException::new_err(e.to_string()))?;
tapo::python::serde_object_to_py_dict(py, &value)
}
}

View File

@@ -0,0 +1,32 @@
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use tapo::responses::{T110Log, TriggerLogsResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[pyclass(get_all)]
#[allow(missing_docs)]
pub struct TriggerLogsT110Result {
start_id: u64,
sum: u64,
logs: Vec<T110Log>,
}
impl From<TriggerLogsResult<T110Log>> for TriggerLogsT110Result {
fn from(result: TriggerLogsResult<T110Log>) -> Self {
Self {
start_id: result.start_id,
sum: result.sum,
logs: result.logs,
}
}
}
#[pyo3::pymethods]
impl TriggerLogsT110Result {
pub fn to_dict(&self, py: pyo3::Python) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
let value = serde_json::to_value(self)
.map_err(|e| pyo3::exceptions::PyException::new_err(e.to_string()))?;
tapo::python::serde_object_to_py_dict(py, &value)
}
}

View File

@@ -0,0 +1,32 @@
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use tapo::responses::{T300Log, TriggerLogsResult};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[pyclass(get_all)]
#[allow(missing_docs)]
pub struct TriggerLogsT300Result {
start_id: u64,
sum: u64,
logs: Vec<T300Log>,
}
impl From<TriggerLogsResult<T300Log>> for TriggerLogsT300Result {
fn from(result: TriggerLogsResult<T300Log>) -> Self {
Self {
start_id: result.start_id,
sum: result.sum,
logs: result.logs,
}
}
}
#[pyo3::pymethods]
impl TriggerLogsT300Result {
pub fn to_dict(&self, py: pyo3::Python) -> pyo3::PyResult<pyo3::Py<pyo3::types::PyDict>> {
let value = serde_json::to_value(self)
.map_err(|e| pyo3::exceptions::PyException::new_err(e.to_string()))?;
tapo::python::serde_object_to_py_dict(py, &value)
}
}

View File

@@ -0,0 +1,60 @@
pub fn tokio() -> &'static tokio::runtime::Runtime {
use std::sync::OnceLock;
use tokio::runtime::Runtime;
static RT: std::sync::OnceLock<Runtime> = OnceLock::new();
RT.get_or_init(|| Runtime::new().expect("Failed to create tokio runtime"))
}
#[macro_export]
macro_rules! call_handler_constructor {
($self:ident, $constructor:path, $($params:expr),*) => {{
let client = $self.client.clone();
let handler = $crate::runtime::tokio()
.spawn(async move {
$constructor(client, $($params),*)
.await
.map_err($crate::errors::ErrorWrapper)
})
.await
.map_err(anyhow::Error::from)
.map_err($crate::errors::ErrorWrapper::from)??;
handler
}};
}
#[macro_export]
macro_rules! call_handler_method {
($handler:expr, $method:path) => (call_handler_method!($handler, $method,));
($handler:expr, $method:path, discard_result) => (call_handler_method!($handler, $method, discard_result,));
($handler:expr, $method:path, $($param:expr),*) => {{
let result = $crate::runtime::tokio()
.spawn(async move {
let result = $method($handler, $($param),*)
.await
.map_err($crate::errors::ErrorWrapper)?;
Ok::<_, $crate::errors::ErrorWrapper>(result)
})
.await
.map_err(anyhow::Error::from)
.map_err($crate::errors::ErrorWrapper::from)??;
Ok::<_, PyErr>(result)
}};
($handler:expr, $method:path, discard_result, $($param:expr),*) => {{
let result = $crate::runtime::tokio()
.spawn(async move {
$method($handler, $($param),*)
.await
.map_err($crate::errors::ErrorWrapper)?;
Ok::<_, $crate::errors::ErrorWrapper>(())
})
.await
.map_err(anyhow::Error::from)
.map_err($crate::errors::ErrorWrapper::from)??;
Ok(result)
}};
}

View File

@@ -0,0 +1,5 @@
from .tapo import *
__doc__ = tapo.__doc__
if hasattr(tapo, "__all__"):
__all__ = tapo.__all__

View File

@@ -0,0 +1,23 @@
from .api_client import *
from .color_light_handler import *
from .device_discovery import *
from .discovery_result import *
from .generic_device_handler import *
from .hub_handler import *
from .ke100_handler import *
from .light_handler import *
from .plug_energy_monitoring_handler import *
from .plug_handler import *
from .power_strip_energy_monitoring_handler import *
from .power_strip_handler import *
from .power_strip_plug_energy_monitoring_handler import *
from .power_strip_plug_handler import *
from .requests import *
from .responses import *
from .rgb_light_strip_handler import *
from .rgbic_light_strip_handler import *
from .s200b_handler import *
from .t100_handler import *
from .t110_handler import *
from .t300_handler import *
from .t31x_handler import *

View File

@@ -0,0 +1,487 @@
"""Tapo API Client.
Tested with light bulbs (L510, L520, L530, L535, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P110M, P115),
power strips (P300, P304M, P306, P316M), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315).
Example:
```python
import asyncio
from tapo import ApiClient
async def main():
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l530("192.168.1.100")
await device.on()
if __name__ == "__main__":
asyncio.run(main())
```
See [more examples](https://github.com/mihai-dinculescu/tapo/tree/main/tapo-py/examples).
"""
from .color_light_handler import ColorLightHandler
from .device_discovery import DeviceDiscovery
from .generic_device_handler import GenericDeviceHandler
from .hub_handler import HubHandler
from .light_handler import LightHandler
from .plug_energy_monitoring_handler import PlugEnergyMonitoringHandler
from .plug_handler import PlugHandler
from .power_strip_energy_monitoring_handler import PowerStripEnergyMonitoringHandler
from .power_strip_handler import PowerStripHandler
from .rgb_light_strip_handler import RgbLightStripHandler
from .rgbic_light_strip_handler import RgbicLightStripHandler
class ApiClient:
"""Tapo API Client.
Tested with light bulbs (L510, L520, L530, L535, L610, L630), light strips (L900, L920, L930), plugs (P100, P105, P110, P110M, P115),
power strips (P300, P304M, P306, P316M), hubs (H100), switches (S200B) and sensors (KE100, T100, T110, T300, T310, T315).
Example:
```python
import asyncio
from tapo import ApiClient
async def main():
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l530("192.168.1.100")
await device.on()
if __name__ == "__main__":
asyncio.run(main())
```
See [more examples](https://github.com/mihai-dinculescu/tapo/tree/main/tapo-py/examples).
"""
def __init__(self, tapo_username: str, tapo_password: str, timeout_s: int = 30) -> None:
"""Returns a new instance of `ApiClient`.
Args:
tapo_username (str): The Tapo username.
tapo_password (str): The Tapo password.
timeout_s (int): The connection timeout in seconds. The default value is 30 seconds.
Returns:
ApiClient: Tapo API Client.
Example:
```python
import asyncio
from tapo import ApiClient
async def main():
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l530("192.168.1.100")
await device.on()
if __name__ == "__main__":
asyncio.run(main())
```
See [more examples](https://github.com/mihai-dinculescu/tapo/tree/main/tapo-py/examples).
"""
async def discover_devices(self, target: str, timeout_s: int = 10) -> DeviceDiscovery:
"""Discovers one or more devices located at a specified unicast or broadcast IP address.
Args:
target (str): The IP address at which the discovery will take place.
This address can be either a unicast (e.g. `192.168.1.10`) or a
broadcast address (e.g. `192.168.1.255`, `255.255.255.255`, etc.).
timeout_s (int): The maximum time to wait for a response from the device(s) in seconds.
Must be between `1` and `60`.
Returns:
AsyncIterator[MaybeDiscoveryResult]: An asynchronous iterator that yields `MaybeDiscoveryResult` objects.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
async for device in client.discover_devices("192.168.1.255"):
try:
device = discovery_result.get()
match device:
case DiscoveryResult.PlugEnergyMonitoring(device_info):
print(
f"Found '{device_info.nickname}' of model '{device_info.model}' at IP address '{device_info.ip}'."
)
# ...
except Exception as e:
print(f"Error discovering device: {e}")
```
"""
async def generic_device(self, ip_address: str) -> GenericDeviceHandler:
"""Specializes the given `ApiClient` into an authenticated `GenericDeviceHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
GenericDeviceHandler: Handler for generic devices. It provides the
functionality common to all Tapo [devices](https://www.tapo.com/en/).
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.generic_device("192.168.1.100")
await device.on()
```
"""
async def l510(self, ip_address: str) -> LightHandler:
"""Specializes the given `ApiClient` into an authenticated `LightHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
LightHandler: Handler for the [L510](https://www.tapo.com/en/search/?q=L510),
[L520](https://www.tapo.com/en/search/?q=L520) and [L610](https://www.tapo.com/en/search/?q=L610) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l510("192.168.1.100")
await device.on()
```
"""
async def l520(self, ip_address: str) -> LightHandler:
"""Specializes the given `ApiClient` into an authenticated `LightHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
LightHandler: Handler for the [L510](https://www.tapo.com/en/search/?q=L510),
[L520](https://www.tapo.com/en/search/?q=L520) and [L610](https://www.tapo.com/en/search/?q=L610) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l520("192.168.1.100")
await device.on()
```
"""
async def l530(self, ip_address: str) -> ColorLightHandler:
"""Specializes the given `ApiClient` into an authenticated `ColorLightHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
ColorLightHandler: Handler for the [L530](https://www.tapo.com/en/search/?q=L530),
[L535](https://www.tapo.com/en/search/?q=L535) and [L630](https://www.tapo.com/en/search/?q=L630) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l530("192.168.1.100")
await device.on()
```
"""
async def l535(self, ip_address: str) -> ColorLightHandler:
"""Specializes the given `ApiClient` into an authenticated `ColorLightHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
ColorLightHandler: Handler for the [L530](https://www.tapo.com/en/search/?q=L530),
[L535](https://www.tapo.com/en/search/?q=L535) and [L630](https://www.tapo.com/en/search/?q=L630) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l535("192.168.1.100")
await device.on()
```
"""
async def l610(self, ip_address: str) -> LightHandler:
"""Specializes the given `ApiClient` into an authenticated `LightHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
LightHandler: Handler for the [L510](https://www.tapo.com/en/search/?q=L510),
[L520](https://www.tapo.com/en/search/?q=L520) and [L610](https://www.tapo.com/en/search/?q=L610) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l610("192.168.1.100")
await device.on()
```
"""
async def l630(self, ip_address: str) -> ColorLightHandler:
"""Specializes the given `ApiClient` into an authenticated `ColorLightHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
ColorLightHandler: Handler for the [L530](https://www.tapo.com/en/search/?q=L530),
[L630](https://www.tapo.com/en/search/?q=L630) and [L900](https://www.tapo.com/en/search/?q=L900) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l630("192.168.1.100")
await device.on()
```
"""
async def l900(self, ip_address: str) -> RgbLightStripHandler:
"""Specializes the given `ApiClient` into an authenticated `RgbLightStripHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
RgbLightStripHandler: Handler for the [L900](https://www.tapo.com/en/search/?q=L900) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l900("192.168.1.100")
await device.on()
```
"""
async def l920(self, ip_address: str) -> RgbicLightStripHandler:
"""Specializes the given `ApiClient` into an authenticated `RgbicLightStripHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
RgbicLightStripHandler: Handler for the [L920](https://www.tapo.com/en/search/?q=L920) and
[L930](https://www.tapo.com/en/search/?q=L930) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l920("192.168.1.100")
await device.on()
```
"""
async def l930(self, ip_address: str) -> RgbicLightStripHandler:
"""Specializes the given `ApiClient` into an authenticated `RgbicLightStripHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
RgbicLightStripHandler: Handler for the [L920](https://www.tapo.com/en/search/?q=L920) and
[L930](https://www.tapo.com/en/search/?q=L930) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.l930("192.168.1.100")
await device.on()
```
"""
async def p100(self, ip_address: str) -> PlugHandler:
"""Specializes the given `ApiClient` into an authenticated `PlugHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PlugHandler: Handler for the [P100](https://www.tapo.com/en/search/?q=P100) and
[P105](https://www.tapo.com/en/search/?q=P105) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.p100("192.168.1.100")
await device.on()
```
"""
async def p105(self, ip_address: str) -> PlugHandler:
"""Specializes the given `ApiClient` into an authenticated `PlugHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PlugHandler: Handler for the [P100](https://www.tapo.com/en/search/?q=P100) and
[P105](https://www.tapo.com/en/search/?q=P105) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.p105("192.168.1.100")
await device.on()
```
"""
async def p110(self, ip_address: str) -> PlugEnergyMonitoringHandler:
"""Specializes the given `ApiClient` into an authenticated `PlugEnergyMonitoringHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PlugEnergyMonitoringHandler: Handler for the [P110](https://www.tapo.com/en/search/?q=P110),
[P110M](https://www.tapo.com/en/search/?q=P110M) and
[P115](https://www.tapo.com/en/search/?q=P115) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.p110("192.168.1.100")
await device.on()
```
"""
async def p115(self, ip_address: str) -> PlugEnergyMonitoringHandler:
"""Specializes the given `ApiClient` into an authenticated `PlugEnergyMonitoringHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PlugEnergyMonitoringHandler: Handler for the [P110](https://www.tapo.com/en/search/?q=P110),
[P110M](https://www.tapo.com/en/search/?q=P110M) and
[P115](https://www.tapo.com/en/search/?q=P115) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
device = await client.p115("192.168.1.100")
await device.on()
```
"""
async def p300(self, ip_address: str) -> PowerStripHandler:
"""Specializes the given `ApiClient` into an authenticated `PowerStripHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PowerStripHandler: Handler for the [P300](https://www.tp-link.com/en/search/?q=P300) and
[P306](https://www.tp-link.com/us/search/?q=P306) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
power_strip = await client.p300("192.168.1.100")
child_device_list = await power_strip.get_child_device_list()
print(f"Child device list: {child_device_list.to_dict()}")
```
"""
async def p304(self, ip_address: str) -> PowerStripEnergyMonitoringHandler:
"""Specializes the given `ApiClient` into an authenticated `PowerStripEnergyMonitoringHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PowerStripEnergyMonitoringHandler: Handler for the [P304M](https://www.tp-link.com/uk/search/?q=P304M) and
[P316M](https://www.tp-link.com/us/search/?q=P316M) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
power_strip = await client.p304("192.168.1.100")
child_device_list = await power_strip.get_child_device_list()
print(f"Child device list: {child_device_list.to_dict()}")
```
"""
async def p306(self, ip_address: str) -> PowerStripHandler:
"""Specializes the given `ApiClient` into an authenticated `PowerStripHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PowerStripHandler: Handler for the [P300](https://www.tp-link.com/en/search/?q=P300) and
[P306](https://www.tp-link.com/us/search/?q=P306) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
power_strip = await client.p306("192.168.1.100")
child_device_list = await power_strip.get_child_device_list()
print(f"Child device list: {child_device_list.to_dict()}")
```
"""
async def p316(self, ip_address: str) -> PowerStripHandler:
"""Specializes the given `ApiClient` into an authenticated `PowerStripHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
PowerStripEnergyMonitoringHandler: Handler for the [P304M](https://www.tp-link.com/uk/search/?q=P304M) and
[P316M](https://www.tp-link.com/us/search/?q=P316M) devices.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
power_strip = await client.p316("192.168.1.100")
child_device_list = await power_strip.get_child_device_list()
print(f"Child device list: {child_device_list.to_dict()}")
```
"""
async def h100(self, ip_address: str) -> HubHandler:
"""Specializes the given `ApiClient` into an authenticated `HubHandler`.
Args:
ip_address (str): The IP address of the device
Returns:
HubHandler: Handler for the [H100](https://www.tapo.com/en/search/?q=H100) hubs.
Example:
```python
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
child_device_list = await hub.get_child_device_list()
print(f"Child device list: {child_device_list.to_dict()}")
```
"""

View File

@@ -0,0 +1,88 @@
from tapo.device_management_ext import DeviceManagementExt
from tapo.requests import Color, ColorLightSetDeviceInfoParams
from tapo.responses import DeviceInfoColorLightResult, DeviceUsageResult
class ColorLightHandler(DeviceManagementExt):
"""Handler for the [L530](https://www.tapo.com/en/search/?q=L530),
[L535](https://www.tapo.com/en/search/?q=L535) and
[L630](https://www.tapo.com/en/search/?q=L630) devices.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def on(self) -> None:
"""Turns *on* the device."""
async def off(self) -> None:
"""Turns *off* the device."""
async def get_device_info(self) -> DeviceInfoColorLightResult:
"""Returns *device info* as `DeviceInfoColorLightResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `ColorLightHandler.get_device_info_json`.
Returns:
DeviceInfoColorLightResult: Device info of Tapo L530, L535 and L630.
Superset of `GenericDeviceInfoResult`.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_device_usage(self) -> DeviceUsageResult:
"""Returns *device usage* as `DeviceUsageResult`.
Returns:
DeviceUsageResult: Contains the time usage.
"""
def set(self) -> ColorLightSetDeviceInfoParams:
"""Returns a `ColorLightSetDeviceInfoParams` builder that allows
multiple properties to be set in a single request.
`ColorLightSetDeviceInfoParams.send` must be called at the end to apply the changes.
Returns:
ColorLightSetDeviceInfoParams: Builder that is used by the
`ColorLightHandler.set` API to set multiple properties in a single request.
"""
async def set_brightness(self, brightness: int) -> None:
"""Sets the *brightness* and turns *on* the device.
Args:
brightness (int): between 1 and 100
"""
async def set_color(self, color: Color) -> None:
"""Sets the *color* and turns *on* the device.
Args:
color (Color): one of `tapo.Color` as defined in the Google Home app.
"""
async def set_hue_saturation(self, hue: int, saturation: int) -> None:
"""Sets the *hue*, *saturation* and turns *on* the device.
Args:
hue (int): between 0 and 360
saturation (int): between 1 and 100
"""
async def set_color_temperature(self, color_temperature: int) -> None:
"""Sets the *color temperature* and turns *on* the device.
Args:
color_temperature (int): between 2500 and 6500
"""

View File

@@ -0,0 +1,14 @@
from __future__ import annotations
from typing import AsyncIterable, Iterable, Iterator, AsyncIterator
from .discovery_result import MaybeDiscoveryResult
class DeviceDiscoveryIter(Iterator[MaybeDiscoveryResult], AsyncIterator[MaybeDiscoveryResult]):
def __iter__(self) -> DeviceDiscoveryIter: ...
def __aiter__(self) -> DeviceDiscoveryIter: ...
def __next__(self) -> MaybeDiscoveryResult: ...
async def __anext__(self) -> MaybeDiscoveryResult: ...
class DeviceDiscovery(Iterable[MaybeDiscoveryResult], AsyncIterable[MaybeDiscoveryResult]):
def __iter__(self) -> DeviceDiscoveryIter: ...
def __aiter__(self) -> DeviceDiscoveryIter: ...

View File

@@ -0,0 +1,27 @@
class DeviceManagementExt:
"""Extension class for device management capabilities like `device_reboot` and `device_reset`."""
async def device_reboot(self, delay_s: int) -> None:
"""*Reboots* the device.
Notes:
* Using a very small delay (e.g. 0 seconds) may cause a `ConnectionReset` or `TimedOut` error as the device reboots immediately.
* Using a larger delay (e.g. 2-3 seconds) allows the device to respond before rebooting, reducing the chance of errors.
* With larger delays, the method completes successfully before the device reboots.
However, subsequent commands may fail if sent during the reboot process or before the device reconnects to the network.
Args:
delay_s (int): The delay in seconds before the device is rebooted.
"""
async def device_reset(self) -> None:
"""*Hardware resets* the device.
Warning:
This action will reset the device to its factory settings.
The connection to the Wi-Fi network and the Tapo app will be lost,
and the device will need to be reconfigured.
This feature is especially useful when the device is difficult to access
and requires reconfiguration.
"""

View File

@@ -0,0 +1,238 @@
from dataclasses import dataclass
from typing import Type, Union
from tapo import (
ColorLightHandler,
GenericDeviceHandler,
HubHandler,
LightHandler,
PlugEnergyMonitoringHandler,
PlugHandler,
PowerStripEnergyMonitoringHandler,
PowerStripHandler,
RgbicLightStripHandler,
RgbLightStripHandler,
)
from tapo.responses import (
DeviceInfoColorLightResult,
DeviceInfoGenericResult,
DeviceInfoHubResult,
DeviceInfoLightResult,
DeviceInfoPlugEnergyMonitoringResult,
DeviceInfoPlugResult,
DeviceInfoPowerStripResult,
DeviceInfoRgbicLightStripResult,
DeviceInfoRgbLightStripResult,
)
class MaybeDiscoveryResult:
"""Potential result of the device discovery process. Using `get` will return the actual result or raise an exception."""
def get(
self,
) -> Union[
GenericDevice,
Light,
ColorLight,
RgbLightStrip,
RgbicLightStrip,
Plug,
PlugEnergyMonitoring,
PowerStrip,
PowerStripEnergyMonitoring,
Hub,
]:
"""Retrieves the actual discovery result or raises an exception."""
@dataclass
class GenericDevice:
"""A Generic Tapo device.
If you believe this device is already supported, or would like to explore adding support for a currently
unsupported model, please [open an issue on GitHub](https://github.com/mihai-dinculescu/tapo/issues)
to start the discussion.
"""
device_info: DeviceInfoGenericResult
"""Device info of a Generic Tapo device.
If you believe this device is already supported, or would like to explore adding support for a currently
unsupported model, please [open an issue on GitHub](https://github.com/mihai-dinculescu/tapo/issues)
to start the discussion.
"""
handler: GenericDeviceHandler
"""Handler for generic devices. It provides the functionality common to all Tapo [devices](https://www.tapo.com/en/).
If you believe this device is already supported, or would like to explore adding support for a currently
unsupported model, please [open an issue on GitHub](https://github.com/mihai-dinculescu/tapo/issues)
to start the discussion.
"""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class Light:
"""Tapo L510, L520 and L610 devices."""
device_info: DeviceInfoLightResult
"""Device info of Tapo L510, L520 and L610."""
handler: LightHandler
"""Handler for the [L510](https://www.tapo.com/en/search/?q=L510),
[L520](https://www.tapo.com/en/search/?q=L520) and
[L610](https://www.tapo.com/en/search/?q=L610) devices.
"""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class ColorLight:
"""Tapo L530, L535 and L630 devices."""
device_info: DeviceInfoColorLightResult
"""Device info of Tapo L530, L535 and L630."""
handler: ColorLightHandler
"""Handler for the [L530](https://www.tapo.com/en/search/?q=L530),
[L535](https://www.tapo.com/en/search/?q=L535) and
[L630](https://www.tapo.com/en/search/?q=L630) devices.
"""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class RgbLightStrip:
"""Tapo L900 devices."""
device_info: DeviceInfoRgbLightStripResult
"""Device info of Tapo L900."""
handler: RgbLightStripHandler
"""Handler for the [L900](https://www.tapo.com/en/search/?q=L900) devices."""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class RgbicLightStrip:
"""Tapo L920 and L930 devices."""
device_info: DeviceInfoRgbicLightStripResult
"""Device info of Tapo L920 and L930."""
handler: RgbicLightStripHandler
"""Handler for the [L920](https://www.tapo.com/en/search/?q=L920) and
[L930](https://www.tapo.com/en/search/?q=L930) devices."""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class Plug:
"""Tapo P100 and P105 devices."""
device_info: DeviceInfoPlugResult
"""Device info of Tapo P100 and P105."""
handler: PlugHandler
"""Handler for the [P100](https://www.tapo.com/en/search/?q=P100) and
[P105](https://www.tapo.com/en/search/?q=P105) devices."""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class PlugEnergyMonitoring:
"""Tapo P110, P110M and P115 devices."""
device_info: DeviceInfoPlugEnergyMonitoringResult
"""Device info of Tapo P110, P110M and P115."""
handler: PlugEnergyMonitoringHandler
"""Handler for the [P110](https://www.tapo.com/en/search/?q=P110),
[P110M](https://www.tapo.com/en/search/?q=P110M) and
[P115](https://www.tapo.com/en/search/?q=P115) devices."""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class PowerStrip:
"""Tapo P300 and P306 devices."""
device_info: DeviceInfoPowerStripResult
"""Device info of Tapo P300 and P306."""
handler: PowerStripHandler
"""Handler for the [P300](https://www.tp-link.com/en/search/?q=P300) and
[P306](https://www.tp-link.com/us/search/?q=P306) devices.
"""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class PowerStripEnergyMonitoring:
"""Tapo P304M and P316M devices."""
device_info: DeviceInfoPowerStripResult
"""Device info of Tapo P304M and P316M."""
handler: PowerStripEnergyMonitoringHandler
"""Handler for the [P304M](https://www.tp-link.com/uk/search/?q=P304M) and
[P316M](https://www.tp-link.com/us/search/?q=P316M) devices.
"""
__match_args__ = (
"device_info",
"handler",
)
@dataclass
class Hub:
"""Tapo H100 devices."""
device_info: DeviceInfoHubResult
"""Device info of Tapo H100."""
handler: HubHandler
"""Handler for the [H100](https://www.tapo.com/en/search/?q=H100) devices."""
__match_args__ = (
"device_info",
"handler",
)
class DiscoveryResult:
"""Result of the device discovery process."""
GenericDevice: Type[GenericDevice] = GenericDevice
Light: Type[Light] = Light
ColorLight: Type[ColorLight] = ColorLight
RgbLightStrip: Type[RgbLightStrip] = RgbLightStrip
RgbicLightStrip: Type[RgbicLightStrip] = RgbicLightStrip
Plug: Type[Plug] = Plug
PlugEnergyMonitoring: Type[PlugEnergyMonitoring] = PlugEnergyMonitoring
PowerStrip: Type[PowerStrip] = PowerStrip
PowerStripEnergyMonitoring: Type[PowerStripEnergyMonitoring] = PowerStripEnergyMonitoring
Hub: Type[Hub] = Hub

View File

@@ -0,0 +1,41 @@
from tapo.responses import DeviceInfoGenericResult
class GenericDeviceHandler:
"""Handler for generic devices. It provides the functionality common to
all Tapo [devices](https://www.tapo.com/en/).
If you'd like to propose support for a device that isn't currently supported,
please [open an issue on GitHub](https://github.com/mihai-dinculescu/tapo/issues) to start the conversation.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def on(self) -> None:
"""Turns *on* the device."""
async def off(self) -> None:
"""Turns *off* the device."""
async def get_device_info(self) -> DeviceInfoGenericResult:
"""Returns *device info* as `DeviceInfoGenericResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `GenericDeviceHandler.get_device_info_json`.
Returns:
DeviceInfoGenericResult: Device info of a Generic Tapo device.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""

View File

@@ -0,0 +1,295 @@
from typing import List, Optional, Union
from tapo import KE100Handler, S200BHandler, T100Handler, T110Handler, T300Handler, T31XHandler
from tapo.device_management_ext import DeviceManagementExt
from tapo.requests import AlarmDuration, AlarmRingtone, AlarmVolume
from tapo.responses import (
DeviceInfoHubResult,
KE100Result,
S200BResult,
T100Result,
T110Result,
T300Result,
T31XResult,
)
class HubHandler(DeviceManagementExt):
"""Handler for the [H100](https://www.tapo.com/en/search/?q=H100) devices."""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def get_device_info(self) -> DeviceInfoHubResult:
"""Returns *device info* as `DeviceInfoHubResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `HubHandler.get_device_info_json`.
Returns:
DeviceInfoHubResult: Device info of Tapo H100.
Superset of `GenericDeviceInfoResult`.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_list(
self,
) -> List[
Union[KE100Result, S200BResult, T100Result, T110Result, T300Result, T31XResult, None]
]:
"""Returns *child device list* as `List[KE100Result | S200BResult | T100Result | T110Result | T300Result | T31XResult | None]`.
It is not guaranteed to contain all the properties returned from the Tapo API
or to support all the possible devices connected to the hub.
If the deserialization fails, or if a property that you care about it's not present,
try `HubHandler.get_child_device_list_json`.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_list_json(self, start_index: int) -> dict:
"""Returns *child device list* as json.
It contains all the properties returned from the Tapo API.
Args:
start_index (int): the index to start fetching the child device list.
It should be `0` for the first page, `10` for the second, and so on.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_component_list_json(self) -> dict:
"""Returns *child device component list* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_supported_ringtone_list() -> List[str]:
"""Returns a list of ringtones (alarm types) supported by the hub.
Used for debugging only.
Returns:
List[str]: List of the ringtones supported by the hub.
"""
async def play_alarm(
self,
ringtone: AlarmRingtone,
volume: AlarmVolume,
duration: AlarmDuration,
seconds: Optional[int] = None,
) -> None:
"""Start playing the hub alarm.
Args:
ringtone (AlarmRingtone): The ringtone of a H100 alarm.
volume (AlarmVolume): The volume of the alarm.
duration (AlarmDuration): Controls how long the alarm plays for.
seconds (Optional[int]): Play the alarm a number of seconds. Required if `duration` is `AlarmDuration.Seconds`.
"""
async def stop_alarm(self) -> None:
"""Stop playing the hub alarm, if it's currently playing."""
async def ke100(
self, device_id: Optional[str] = None, nickname: Optional[str] = None
) -> KE100Handler:
"""Returns a `KE100Handler` for the device matching the provided `device_id` or `nickname`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
Returns:
KE100Handler: Handler for the [KE100](https://www.tp-link.com/en/search/?q=KE100) devices.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
# Get a handler for the child device
device = await hub.ke100(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""
async def s200b(
self, device_id: Optional[str] = None, nickname: Optional[str] = None
) -> S200BHandler:
"""Returns a `S200BHandler` for the device matching the provided `device_id` or `nickname`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
Returns:
S200BHandler: Handler for the [S200B](https://www.tapo.com/en/search/?q=S200B) devices.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
# Get a handler for the child device
device = await hub.s200b(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""
async def t100(
self, device_id: Optional[str] = None, nickname: Optional[str] = None
) -> T100Handler:
"""Returns a `T100Handler` for the device matching the provided `device_id` or `nickname`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
Returns:
T100Handler: Handler for the [T100](https://www.tapo.com/en/search/?q=T100) devices.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
# Get a handler for the child device
device = await hub.t100(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""
async def t110(
self, device_id: Optional[str] = None, nickname: Optional[str] = None
) -> T110Handler:
"""Returns a `T110Handler` for the device matching the provided `device_id` or `nickname`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
Returns:
T110Handler: Handler for the [T110](https://www.tapo.com/en/search/?q=T110) devices.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
# Get a handler for the child device
device = await hub.t110(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""
async def t300(
self, device_id: Optional[str] = None, nickname: Optional[str] = None
) -> T300Handler:
"""Returns a `T300Handler` for the device matching the provided `device_id` or `nickname`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
Returns:
T300Handler: Handler for the [T300](https://www.tapo.com/en/search/?q=T300) devices.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
# Get a handler for the child device
device = await hub.t300(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""
async def t310(
self, device_id: Optional[str] = None, nickname: Optional[str] = None
) -> T31XHandler:
"""Returns a `T31XHandler` for the device matching the provided `device_id` or `nickname`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
Returns:
T31XHandler: Handler for the [T310](https://www.tapo.com/en/search/?q=T310)
and [T315](https://www.tapo.com/en/search/?q=T315) devices.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
# Get a handler for the child device
device = await hub.t310(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""
async def t315(
self, device_id: Optional[str] = None, nickname: Optional[str] = None
) -> T31XHandler:
"""Returns a `T31XHandler` for the device matching the provided `device_id` or `nickname`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
Returns:
T31XHandler: Handler for the [T310](https://www.tapo.com/en/search/?q=T310)
and [T315](https://www.tapo.com/en/search/?q=T315) devices.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
hub = await client.h100("192.168.1.100")
# Get a handler for the child device
device = await hub.t315(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""

View File

@@ -0,0 +1,69 @@
from tapo.requests import TemperatureUnitKE100
from tapo.responses import KE100Result
class KE100Handler:
"""Handler for the [KE100](https://www.tp-link.com/en/search/?q=KE100) devices."""
async def get_device_info(self) -> KE100Result:
"""Returns *device info* as `KE100Result`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `KE100Handler.get_device_info_json`.
Returns:
KE100Result: Device info of Tapo KE100 thermostatic radiator valve (TRV).
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def set_child_protection(self, on: bool) -> None:
"""Sets *child protection* on the device to *on* or *off*.
Args:
on (bool)
"""
async def set_frost_protection(self, on: bool) -> None:
"""Sets *frost protection* on the device to *on* or *off*.
Args:
on (bool)
"""
async def set_max_control_temperature(self, value: int, unit: TemperatureUnitKE100) -> None:
"""Sets the *maximum control temperature*.
Args:
value (int)
unit (TemperatureUnitKE100)
"""
async def set_min_control_temperature(self, value: int, unit: TemperatureUnitKE100) -> None:
"""Sets the *minimum control temperature*.
Args:
value (int)
unit (TemperatureUnitKE100)
"""
async def set_target_temperature(self, value: int, unit: TemperatureUnitKE100) -> None:
"""Sets the *target temperature*.
Args:
value (int): between `min_control_temperature` and `max_control_temperature`
unit (TemperatureUnitKE100)
"""
async def set_temperature_offset(self, value: int, unit: TemperatureUnitKE100) -> None:
"""Sets the *temperature offset*.
Args:
value (int): between -10 and 10
unit (TemperatureUnitKE100)
"""

View File

@@ -0,0 +1,54 @@
from tapo.device_management_ext import DeviceManagementExt
from tapo.responses import DeviceInfoLightResult, DeviceUsageResult
class LightHandler(DeviceManagementExt):
"""Handler for the [L510](https://www.tapo.com/en/search/?q=L510),
[L520](https://www.tapo.com/en/search/?q=L520) and
[L610](https://www.tapo.com/en/search/?q=L610) devices."""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def on(self) -> None:
"""Turns *on* the device."""
async def off(self) -> None:
"""Turns *off* the device."""
async def get_device_info(self) -> DeviceInfoLightResult:
"""Returns *device info* as `DeviceInfoLightResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `LightHandler.get_device_info_json`.
Returns:
DeviceInfoLightResult: Device info of Tapo L510, L520 and L610.
Superset of `GenericDeviceInfoResult`.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_device_usage(self) -> DeviceUsageResult:
"""Returns *device usage* as `DeviceUsageResult`.
Returns:
DeviceUsageResult: Contains the time usage.
"""
async def set_brightness(self, brightness: int) -> None:
"""Sets the *brightness* and turns *on* the device.
Args:
brightness (int): between 1 and 100
"""

View File

@@ -0,0 +1,98 @@
from datetime import datetime
from tapo.device_management_ext import DeviceManagementExt
from tapo.requests import EnergyDataInterval, PowerDataInterval
from tapo.responses import (
CurrentPowerResult,
DeviceInfoPlugEnergyMonitoringResult,
DeviceUsageEnergyMonitoringResult,
EnergyDataResult,
EnergyUsageResult,
PowerDataResult,
)
class PlugEnergyMonitoringHandler(DeviceManagementExt):
"""Handler for the [P110](https://www.tapo.com/en/search/?q=P110),
[P110M](https://www.tapo.com/en/search/?q=P110M) and
[P115](https://www.tapo.com/en/search/?q=P115) devices.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def on(self) -> None:
"""Turns *on* the device."""
async def off(self) -> None:
"""Turns *off* the device."""
async def get_device_info(self) -> DeviceInfoPlugEnergyMonitoringResult:
"""Returns *device info* as `DeviceInfoPlugEnergyMonitoringResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PlugEnergyMonitoringHandler.get_device_info_json`.
Returns:
DeviceInfoPlugEnergyMonitoringResult: Device info of P110, P110M and P115.
Superset of `GenericDeviceInfoResult`.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_current_power(self) -> CurrentPowerResult:
"""Returns *current power* as `CurrentPowerResult`.
Returns:
CurrentPowerResult: Contains the current power reading of the device.
"""
async def get_device_usage(self) -> DeviceUsageEnergyMonitoringResult:
"""Returns *device usage* as `DeviceUsageResult`.
Returns:
DeviceUsageEnergyMonitoringResult:
Contains the time usage, the power consumption, and the energy savings of the device.
"""
async def get_energy_usage(self) -> EnergyUsageResult:
"""Returns *energy usage* as `EnergyUsageResult`.
Returns:
EnergyUsageResult:
Contains local time, current power and the energy usage and runtime for today and for the current month.
"""
async def get_energy_data(
self,
interval: EnergyDataInterval,
start_date: datetime,
end_date: datetime = None,
) -> EnergyDataResult:
"""Returns *energy data* as `EnergyDataResult`.
Returns:
EnergyDataResult: Energy data result for the requested `EnergyDataInterval`.
"""
async def get_power_data(
self,
interval: PowerDataInterval,
start_date_time: datetime,
end_date_time: datetime,
) -> PowerDataResult:
"""Returns *power data* as `PowerDataResult`.
Returns:
PowerDataResult: Power data result for the requested `PowerDataInterval`.
"""

View File

@@ -0,0 +1,47 @@
from tapo.device_management_ext import DeviceManagementExt
from tapo.responses import DeviceInfoPlugResult, DeviceUsageResult
class PlugHandler(DeviceManagementExt):
"""Handler for the [P100](https://www.tapo.com/en/search/?q=P100) and
[P105](https://www.tapo.com/en/search/?q=P105) devices.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def on(self) -> None:
"""Turns *on* the device."""
async def off(self) -> None:
"""Turns *off* the device."""
async def get_device_info(self) -> DeviceInfoPlugResult:
"""Returns *device info* as `DeviceInfoPlugResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PlugHandler.get_device_info_json`.
Returns:
DeviceInfoPlugResult: Device info of Tapo P100 and P105.
Superset of `GenericDeviceInfoResult`.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_device_usage(self) -> DeviceUsageResult:
"""Returns *device usage* as `DeviceUsageResult`.
Returns:
DeviceUsageResult: Contains the time usage.
"""

View File

@@ -0,0 +1,96 @@
from typing import List, Optional
from tapo import PowerStripPlugEnergyMonitoringHandler
from tapo.device_management_ext import DeviceManagementExt
from tapo.responses import DeviceInfoPowerStripResult, PowerStripPlugEnergyMonitoringResult
class PowerStripEnergyMonitoringHandler(DeviceManagementExt):
"""Handler for the [P304M](https://www.tp-link.com/uk/search/?q=P304M) and
[P316M](https://www.tp-link.com/us/search/?q=P316M) devices.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def get_device_info(self) -> DeviceInfoPowerStripResult:
"""Returns *device info* as `DeviceInfoPowerStripResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PowerStripEnergyMonitoringHandler.get_device_info_json`.
Returns:
DeviceInfoPowerStripResult: Device info of Tapo P300, P304M, P306 and P316M. Superset of `DeviceInfoGenericResult`.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_list(
self,
) -> List[PowerStripPlugEnergyMonitoringResult]:
"""Returns *child device list* as `List[PowerStripPlugEnergyMonitoringResult]`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PowerStripEnergyMonitoringHandler.get_child_device_list_json`.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_list_json(self) -> dict:
"""Returns *child device list* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_component_list_json(self) -> dict:
"""Returns *child device component list* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def plug(
self,
device_id: Optional[str] = None,
nickname: Optional[str] = None,
position: Optional[int] = None,
) -> PowerStripPlugEnergyMonitoringHandler:
"""Returns a `PowerStripPlugEnergyMonitoringHandler` for the device matching the provided `device_id`, `nickname`, or `position`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
position (Optional[str]): The Position of the device
Returns:
PowerStripPlugEnergyMonitoringHandler: Handler for the [P304M](https://www.tp-link.com/uk/search/?q=P304M) and
[P316M](https://www.tp-link.com/us/search/?q=P316M) child plugs.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
power_strip = await client.p304("192.168.1.100")
# Get a handler for the child device
device = await power_strip.plug(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""

View File

@@ -0,0 +1,96 @@
from typing import List, Optional
from tapo import PowerStripPlugHandler
from tapo.device_management_ext import DeviceManagementExt
from tapo.responses import DeviceInfoPowerStripResult, PowerStripPlugResult
class PowerStripHandler(DeviceManagementExt):
"""Handler for the [P300](https://www.tp-link.com/en/search/?q=P300) and
[P306](https://www.tp-link.com/us/search/?q=P306) devices.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def refresh_session(self) -> None:
"""Refreshes the authentication session."""
async def get_device_info(self) -> DeviceInfoPowerStripResult:
"""Returns *device info* as `DeviceInfoPowerStripResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PowerStripHandler.get_device_info_json`.
Returns:
DeviceInfoPowerStripResult: Device info of Tapo P300, P304M, P306 and P316M. Superset of `DeviceInfoGenericResult`.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_list(
self,
) -> List[PowerStripPlugResult]:
"""Returns *child device list* as `List[PowerStripPlugResult]`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PowerStripHandler.get_child_device_list_json`.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_list_json(self) -> dict:
"""Returns *child device list* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_child_device_component_list_json(self) -> dict:
"""Returns *child device component list* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def plug(
self,
device_id: Optional[str] = None,
nickname: Optional[str] = None,
position: Optional[int] = None,
) -> PowerStripPlugHandler:
"""Returns a `PowerStripPlugHandler` for the device matching the provided `device_id`, `nickname`, or `position`.
Args:
device_id (Optional[str]): The Device ID of the device
nickname (Optional[str]): The Nickname of the device
position (Optional[str]): The Position of the device
Returns:
PowerStripPlugHandler: Handler for the [P300](https://www.tp-link.com/en/search/?q=P300) and
[P306](https://www.tp-link.com/us/search/?q=P306) child plugs.
Example:
```python
# Connect to the hub
client = ApiClient("tapo-username@example.com", "tapo-password")
power_strip = await client.p300("192.168.1.100")
# Get a handler for the child device
device = await power_strip.plug(device_id="0000000000000000000000000000000000000000")
# Get the device info of the child device
device_info = await device.get_device_info()
print(f"Device info: {device_info.to_dict()}")
```
"""

View File

@@ -0,0 +1,92 @@
from datetime import datetime
from tapo.requests import EnergyDataInterval, PowerDataInterval
from tapo.responses import (
CurrentPowerResult,
DeviceUsageEnergyMonitoringResult,
EnergyDataResult,
EnergyUsageResult,
PowerDataResult,
PowerStripPlugEnergyMonitoringResult,
)
class PowerStripPlugEnergyMonitoringHandler:
"""Handler for the [P304M](https://www.tp-link.com/uk/search/?q=P304M) and
[P316M](https://www.tp-link.com/us/search/?q=P316M) child plugs.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def on(self) -> None:
"""Turns *on* the device."""
async def off(self) -> None:
"""Turns *off* the device."""
async def get_device_info(self) -> PowerStripPlugEnergyMonitoringResult:
"""Returns *device info* as `PowerStripPlugEnergyMonitoringResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PowerStripPlugEnergyMonitoringHandler.get_device_info_json`.
Returns:
PowerStripPlugEnergyMonitoringResult: P304M and P316M power strip child plugs.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""
async def get_current_power(self) -> CurrentPowerResult:
"""Returns *current power* as `CurrentPowerResult`.
Returns:
CurrentPowerResult: Contains the current power reading of the device.
"""
async def get_device_usage(self) -> DeviceUsageEnergyMonitoringResult:
"""Returns *device usage* as `DeviceUsageResult`.
Returns:
DeviceUsageEnergyMonitoringResult:
Contains the time usage, the power consumption, and the energy savings of the device.
"""
async def get_energy_usage(self) -> EnergyUsageResult:
"""Returns *energy usage* as `EnergyUsageResult`.
Returns:
EnergyUsageResult:
Contains local time, current power and the energy usage and runtime for today and for the current month.
"""
async def get_energy_data(
self,
interval: EnergyDataInterval,
start_date: datetime,
end_date: datetime = None,
) -> EnergyDataResult:
"""Returns *energy data* as `EnergyDataResult`.
Returns:
EnergyDataResult: Energy data result for the requested `EnergyDataInterval`.
"""
async def get_power_data(
self,
interval: PowerDataInterval,
start_date_time: datetime,
end_date_time: datetime,
) -> PowerDataResult:
"""Returns *power data* as `PowerDataResult`.
Returns:
PowerDataResult: Power data result for the requested `PowerDataInterval`.
"""

View File

@@ -0,0 +1,35 @@
from tapo.responses import PowerStripPlugResult
class PowerStripPlugHandler:
"""Handler for the [P300](https://www.tp-link.com/en/search/?q=P300) and
[P306](https://www.tp-link.com/us/search/?q=P306) child plugs.
"""
def __init__(self, handler: object):
"""Private constructor.
It should not be called from outside the tapo library.
"""
async def on(self) -> None:
"""Turns *on* the device."""
async def off(self) -> None:
"""Turns *off* the device."""
async def get_device_info(self) -> PowerStripPlugResult:
"""Returns *device info* as `PowerStripPlugResult`.
It is not guaranteed to contain all the properties returned from the Tapo API.
If the deserialization fails, or if a property that you care about it's not present,
try `PowerStripPlugHandler.get_device_info_json`.
Returns:
PowerStripPlugResult: P300 and P306 power strip child plugs.
"""
async def get_device_info_json(self) -> dict:
"""Returns *device info* as json.
It contains all the properties returned from the Tapo API.
Returns:
dict: Device info as a dictionary.
"""

View File

@@ -0,0 +1,6 @@
from .energy_data_interval import *
from .play_alarm import *
from .power_data_interval import *
from .set_device_info import *
from tapo.responses import TemperatureUnitKE100 as TemperatureUnitKE100

View File

@@ -0,0 +1,15 @@
from enum import Enum
class EnergyDataInterval(str, Enum):
"""Energy data interval."""
Hourly = "Hourly"
"""Hourly interval. `start_date` and `end_date` are an inclusive interval
that must not be greater than 8 days.
"""
Daily = "Daily"
"""Daily interval. `start_date` must be the first day of a quarter."""
Monthly = "Monthly"
"""Monthly interval. `start_date` must be the first day of a year."""

View File

@@ -0,0 +1,103 @@
from enum import Enum
class AlarmVolume(str, Enum):
"""The volume of the alarm.
For the H100, this is a fixed list of volume levels."""
Default = "Default"
"""Use the default volume for the hub."""
Mute = "Mute"
"""Mute the audio output from the alarm.
This causes the alarm to be shown as triggered in the Tapo App
without an audible sound, and makes the `in_alarm` property
in `DeviceInfoHubResult` return as `True`."""
Low = "Low"
"""Lowest volume."""
Normal = "Normal"
"""Normal volume. This is the default."""
High = "High"
"""Highest volume."""
class AlarmRingtone(str, Enum):
"""The ringtone of a H100 alarm."""
Alarm1 = "Alarm1"
"""Alarm 1"""
Alarm2 = "Alarm2"
"""Alarm 2"""
Alarm3 = "Alarm3"
"""Alarm 3"""
Alarm4 = "Alarm4"
"""Alarm 4"""
Alarm5 = "Alarm5"
"""Alarm 5"""
Connection1 = "Connection1"
"""Connection 1"""
Connection2 = "Connection2"
"""Connection 2"""
DoorbellRing1 = "DoorbellRing1"
"""Doorbell Ring 1"""
DoorbellRing2 = "DoorbellRing2"
"""Doorbell Ring 2"""
DoorbellRing3 = "DoorbellRing3"
"""Doorbell Ring 3"""
DoorbellRing4 = "DoorbellRing4"
"""Doorbell Ring 4"""
DoorbellRing5 = "DoorbellRing5"
"""Doorbell Ring 5"""
DoorbellRing6 = "DoorbellRing6"
"""Doorbell Ring 6"""
DoorbellRing7 = "DoorbellRing7"
"""Doorbell Ring 7"""
DoorbellRing8 = "DoorbellRing8"
"""Doorbell Ring 8"""
DoorbellRing9 = "DoorbellRing9"
"""Doorbell Ring 9"""
DoorbellRing10 = "DoorbellRing10"
"""Doorbell Ring 10"""
DrippingTap = "DrippingTap"
"""Dripping Tap"""
PhoneRing = "PhoneRing"
"""Phone Ring"""
class AlarmDuration(str, Enum):
"""Controls how long the alarm plays for."""
Continuous = "Continuous"
"""Play the alarm continuously until stopped."""
Once = "Once"
"""Play the alarm once.
This is useful for previewing the audio.
Limitations:
The `in_alarm` field of `DeviceInfoHubResult` will not remain `True` for the
duration of the audio track. Each audio track has a different runtime.
Has no observable affect when used in conjunction with `AlarmVolume.Mute`."""
Seconds = "Seconds"
"""Play the alarm a number of seconds."""

View File

@@ -0,0 +1,16 @@
from enum import Enum
class PowerDataInterval(str, Enum):
"""Power data interval."""
Every5Minutes = "5min"
"""Every 5 minutes interval. `start_date_time` and `end_date_time` describe an exclusive interval.
If the result would yield more than 144 entries (i.e. 12 hours),
the `end_date_time` will be adjusted to an earlier date and time.
"""
Hourly = "Hourly"
"""Hourly interval. `start_date_time` and `end_date_time` describe an exclusive interval.
If the result would yield more than 144 entries (i.e. 6 days),
the `end_date_time` will be adjusted to an earlier date and time.
"""

Some files were not shown because too many files have changed in this diff Show More