Skip to content

Commit

Permalink
tapo-py: Add support for the L530, L630, and L900 devices
Browse files Browse the repository at this point in the history
Addresses #123.
  • Loading branch information
mihai-dinculescu committed Jan 21, 2024
1 parent e9d2953 commit 4923824
Show file tree
Hide file tree
Showing 14 changed files with 558 additions and 71 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ file. This change log follows the conventions of

## [Python Unreleased][Unreleased]

### Added

- Added support for the L530, L630, and L900 color light bulbs.

## [Rust v0.7.8][v0.7.8] - 2024-01-22

### Added
Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with l

| Feature | GenericDevice | L510, L520, L610 | L530, L630, L900 | L920, L930 | P100, P105 | P110, P115 |
| --------------------- | ------------: | ---------------: | ---------------: | ---------: | ---------: | ---------: |
| device_reset | | ✅ | ✓ | ✓ | ✅ | ✅ |
| device_reset | | ✅ | ✅ | ✓ | ✅ | ✅ |
| get_current_power | | | | | | ✅ |
| get_device_info | ✅ | ✅ | ✓ | ✓ | ✅ | ✅ |
| get_device_info_json | ✅ | ✅ | ✓ | ✓ | ✅ | ✅ |
| get_device_usage | | ✅ | ✓ | ✓ | ✅ | ✅ |
| get_device_info | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ |
| get_device_info_json | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ |
| get_device_usage | | ✅ | ✅ | ✓ | ✅ | ✅ |
| get_energy_data | | | | | | ✅ |
| get_energy_usage | | | | | | ✅ |
| off | ✅ | ✅ | ✓ | ✓ | ✅ | ✅ |
| on | ✅ | ✅ | ✓ | ✓ | ✅ | ✅ |
| set_brightness | | ✅ | ✓ | ✓ | | |
| set_color | | | ✓ | ✓ | | |
| set_color_temperature | | | ✓ | ✓ | | |
| set_hue_saturation | | | ✓ | ✓ | | |
| off | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ |
| on | ✅ | ✅ | ✅ | ✓ | ✅ | ✅ |
| set_brightness | | ✅ | ✅ | ✓ | | |
| set_color | | | ✅ | ✓ | | |
| set_color_temperature | | | ✅ | ✓ | | |
| set_hue_saturation | | | ✅ | ✓ | | |
| set_lighting_effect | | | | ✓ | | |
| set() API \* | | | ✓ | ✓ | | |

Expand Down
55 changes: 55 additions & 0 deletions tapo-py/examples/tapo_l530.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""L530, L630 & L900 Example"""

import asyncio
import os

from tapo import ApiClient, 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("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())
1 change: 1 addition & 0 deletions tapo-py/examples/tapo_p100.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ async def main():
device_usage = await device.get_device_usage()
print(f"Device usage: {device_usage.to_dict()}")


if __name__ == "__main__":
asyncio.run(main())
27 changes: 26 additions & 1 deletion tapo-py/src/api_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use tapo::ApiClient;

use crate::errors::ErrorWrapper;
use crate::handlers::{
PyGenericDeviceHandler, PyLightHandler, PyPlugEnergyMonitoringHandler, PyPlugHandler,
PyColorLightHandler, PyGenericDeviceHandler, PyLightHandler, PyPlugEnergyMonitoringHandler,
PyPlugHandler,
};

#[pyclass(name = "ApiClient")]
Expand Down Expand Up @@ -46,6 +47,14 @@ impl PyApiClient {
})
}

pub fn l530<'a>(&'a self, ip_address: String, py: Python<'a>) -> PyResult<&'a PyAny> {
let client = self.client.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
let handler = client.l530(ip_address).await.map_err(ErrorWrapper)?;
Ok(PyColorLightHandler::new(handler))
})
}

pub fn l610<'a>(&'a self, ip_address: String, py: Python<'a>) -> PyResult<&'a PyAny> {
let client = self.client.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
Expand All @@ -54,6 +63,22 @@ impl PyApiClient {
})
}

pub fn l630<'a>(&'a self, ip_address: String, py: Python<'a>) -> PyResult<&'a PyAny> {
let client = self.client.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
let handler = client.l630(ip_address).await.map_err(ErrorWrapper)?;
Ok(PyColorLightHandler::new(handler))
})
}

pub fn l900<'a>(&'a self, ip_address: String, py: Python<'a>) -> PyResult<&'a PyAny> {
let client = self.client.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
let handler = client.l900(ip_address).await.map_err(ErrorWrapper)?;
Ok(PyColorLightHandler::new(handler))
})
}

pub fn p100<'a>(&'a self, ip_address: String, py: Python<'a>) -> PyResult<&'a PyAny> {
let client = self.client.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
Expand Down
2 changes: 2 additions & 0 deletions tapo-py/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod color_light_handler;
mod generic_device_handler;
mod light_handler;
mod plug_energy_monitoring_handler;
mod plug_handler;

pub use color_light_handler::*;
pub use generic_device_handler::*;
pub use light_handler::*;
pub use plug_energy_monitoring_handler::*;
Expand Down
168 changes: 168 additions & 0 deletions tapo-py/src/handlers/color_light_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::sync::Arc;

use pyo3::prelude::*;
use tapo::{requests::Color, ColorLightHandler};
use tokio::sync::Mutex;

use crate::errors::ErrorWrapper;

#[derive(Clone)]
#[pyclass(name = "ColorColorLightHandler")]
pub struct PyColorLightHandler {
handler: Arc<Mutex<ColorLightHandler>>,
}

impl PyColorLightHandler {
pub fn new(handler: ColorLightHandler) -> Self {
Self {
handler: Arc::new(Mutex::new(handler)),
}
}
}

#[pymethods]
impl PyColorLightHandler {
pub fn refresh_session<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler
.lock()
.await
.refresh_session()
.await
.map_err(ErrorWrapper)?;
Ok(())
})
}

pub fn on<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler.lock().await.on().await.map_err(ErrorWrapper)?;
Ok(())
})
}

pub fn off<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler.lock().await.off().await.map_err(ErrorWrapper)?;
Ok(())
})
}

pub fn device_reset<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler
.lock()
.await
.device_reset()
.await
.map_err(ErrorWrapper)?;
Ok(())
})
}

pub fn get_device_info<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
let result = handler
.lock()
.await
.get_device_info()
.await
.map_err(ErrorWrapper)?;
Ok(result)
})
}

pub fn get_device_info_json<'a>(&self, py: Python<'a>) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();

pyo3_asyncio::tokio::future_into_py(py, async move {
let result = handler
.lock()
.await
.get_device_info_json()
.await
.map_err(ErrorWrapper)?;

Python::with_gil(|py| tapo::python::serde_object_to_py_dict(py, &result))
})
}

pub fn get_device_usage<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
let result = handler
.lock()
.await
.get_device_usage()
.await
.map_err(ErrorWrapper)?;
Ok(result)
})
}

pub fn set_brightness<'a>(&'a self, py: Python<'a>, brightness: u8) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler
.lock()
.await
.set_brightness(brightness)
.await
.map_err(ErrorWrapper)?;
Ok(())
})
}

pub fn set_color<'a>(&'a self, py: Python<'a>, color: Color) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler
.lock()
.await
.set_color(color)
.await
.map_err(ErrorWrapper)?;
Ok(())
})
}

pub fn set_hue_saturation<'a>(
&'a self,
py: Python<'a>,
hue: u16,
saturation: u8,
) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler
.lock()
.await
.set_hue_saturation(hue, saturation)
.await
.map_err(ErrorWrapper)?;
Ok(())
})
}

pub fn set_color_temperature<'a>(
&'a self,
py: Python<'a>,
color_temperature: u16,
) -> PyResult<&'a PyAny> {
let handler = self.handler.clone();
pyo3_asyncio::tokio::future_into_py(py, async move {
handler
.lock()
.await
.set_color_temperature(color_temperature)
.await
.map_err(ErrorWrapper)?;
Ok(())
})
}
}
13 changes: 8 additions & 5 deletions tapo-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ mod handlers;
use pyo3::prelude::*;

use api_client::PyApiClient;
use handlers::{PyEnergyDataInterval, PyPlugEnergyMonitoringHandler};
use handlers::PyEnergyDataInterval;
use tapo::requests::Color;

#[pymodule]
#[pyo3(name = "tapo")]
fn tapo_py(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<PyApiClient>()?;
m.add_class::<PyPlugEnergyMonitoringHandler>()?;
m.add_class::<PyEnergyDataInterval>()?;
fn tapo_py(_py: Python, module: &PyModule) -> PyResult<()> {
module.add_class::<PyApiClient>()?;

module.add_class::<PyEnergyDataInterval>()?;
module.add_class::<Color>()?;

Ok(())
}
Loading

0 comments on commit 4923824

Please sign in to comment.