Skip to content

Commit

Permalink
tapo-py: Add full support for the KE100 TRV
Browse files Browse the repository at this point in the history
Addresses #128.
  • Loading branch information
mihai-dinculescu committed Nov 3, 2024
1 parent 29e3674 commit 80deac8
Show file tree
Hide file tree
Showing 15 changed files with 343 additions and 86 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 full support for the KE100 thermostatic radiator valve (TRV) through the `KE100Handler`.

## [Rust v0.7.17][v0.7.17] - 2024-10-23

### Added
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ Unofficial Tapo API Client. Works with TP-Link Tapo smart devices. Tested with l
| get_device_info_json | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| get_temperature_humidity_records | | | | | | ✅ |
| get_trigger_logs | | ✅ | ✅ | ✅ | ✅ | |
| set_child_protection | ✓ | | | | | |
| set_frost_protection | ✓ | | | | | |
| set_max_control_temperature | ✓ | | | | | |
| set_min_control_temperature | ✓ | | | | | |
| set_target_temperature | ✓ | | | | | |
| set_temperature_offset | ✓ | | | | | |
| set_child_protection | ✅ | | | | | |
| set_frost_protection | ✅ | | | | | |
| set_max_control_temperature | ✅ | | | | | |
| set_min_control_temperature | ✅ | | | | | |
| set_target_temperature | ✅ | | | | | |
| set_temperature_offset | ✅ | | | | | |

\* Obtained by calling `get_child_device_list` on the hub device or `get_device_info` on a child device handler.

Expand Down
2 changes: 1 addition & 1 deletion tapo-py/examples/tapo_h100.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def main():
child.nickname,
child.device_id,
child.detected,
[log.to_dict() for log in trigger_logs.logs],
[log.to_dict() for log in trigger_logs.logs],
)
)
elif isinstance(child, T110Result):
Expand Down
40 changes: 40 additions & 0 deletions tapo-py/examples/tapo_ke100.py
Original file line number Diff line number Diff line change
@@ -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())
2 changes: 2 additions & 0 deletions tapo-py/src/handlers/child_devices.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
mod ke100_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_handler::*;
pub use s200b_handler::*;
pub use t100_handler::*;
Expand Down
101 changes: 101 additions & 0 deletions tapo-py/src/handlers/child_devices/ke100_handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use std::{ops::Deref, sync::Arc};

use pyo3::{prelude::*, types::PyDict};
use tapo::responses::{KE100Result, TemperatureUnitKE100};
use tapo::KE100Handler;

use crate::call_handler_method;

#[derive(Clone)]
#[pyclass(name = "KE100Handler")]
pub struct PyKE100Handler {
handler: Arc<KE100Handler>,
}

impl PyKE100Handler {
pub fn new(handler: KE100Handler) -> Self {
Self {
handler: Arc::new(handler),
}
}
}

#[pymethods]
impl PyKE100Handler {
pub async fn get_device_info(&self) -> PyResult<KE100Result> {
let handler = self.handler.clone();
call_handler_method!(handler.deref(), KE100Handler::get_device_info)
}

pub async fn get_device_info_json(&self) -> PyResult<Py<PyDict>> {
let handler = self.handler.clone();
let result = call_handler_method!(handler.deref(), KE100Handler::get_device_info_json)?;
Python::with_gil(|py| tapo::python::serde_object_to_py_dict(py, &result))
}

pub async fn set_child_protection(&self, on: bool) -> PyResult<()> {
let handler = self.handler.clone();
call_handler_method!(handler.deref(), KE100Handler::set_child_protection, on)
}

pub async fn set_frost_protection(&self, on: bool) -> PyResult<()> {
let handler = self.handler.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.handler.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.handler.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.handler.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.handler.clone();
call_handler_method!(
handler.deref(),
KE100Handler::set_temperature_offset,
value,
unit
)
}
}
18 changes: 17 additions & 1 deletion tapo-py/src/handlers/hub_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use tokio::sync::RwLock;

use crate::call_handler_method;
use crate::errors::ErrorWrapper;
use crate::handlers::{PyS200BHandler, PyT100Handler, PyT110Handler, PyT300Handler, PyT31XHandler};
use crate::handlers::{
PyKE100Handler, PyS200BHandler, PyT100Handler, PyT110Handler, PyT300Handler, PyT31XHandler,
};

#[derive(Clone)]
#[pyclass(name = "HubHandler")]
Expand Down Expand Up @@ -128,6 +130,20 @@ impl PyHubHandler {
Python::with_gil(|py| tapo::python::serde_object_to_py_dict(py, &result))
}

#[pyo3(signature = (device_id=None, nickname=None))]
pub async fn ke100(
&self,
device_id: Option<String>,
nickname: Option<String>,
) -> PyResult<PyKE100Handler> {
let handler = self.handler.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,
Expand Down
12 changes: 7 additions & 5 deletions tapo-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ use pyo3::prelude::*;
use api_client::PyApiClient;
use handlers::{
PyColorLightHandler, PyColorLightSetDeviceInfoParams, PyEnergyDataInterval,
PyGenericDeviceHandler, PyHubHandler, PyLightHandler, PyPlugEnergyMonitoringHandler,
PyPlugHandler, PyPowerStripHandler, PyPowerStripPlugHandler, PyT100Handler, PyT110Handler,
PyT300Handler, PyT31XHandler, TriggerLogsS200BResult, TriggerLogsT100Result,
TriggerLogsT110Result, TriggerLogsT300Result,
PyGenericDeviceHandler, PyHubHandler, PyKE100Handler, PyLightHandler,
PyPlugEnergyMonitoringHandler, PyPlugHandler, PyPowerStripHandler, PyPowerStripPlugHandler,
PyT100Handler, PyT110Handler, PyT300Handler, PyT31XHandler, TriggerLogsS200BResult,
TriggerLogsT100Result, TriggerLogsT110Result, TriggerLogsT300Result,
};
use pyo3_log::{Caching, Logger};
use tapo::requests::Color;
Expand Down Expand Up @@ -58,9 +58,10 @@ fn tapo_py(py: Python, module: &Bound<'_, PyModule>) -> PyResult<()> {
}

fn register_requests(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<PyEnergyDataInterval>()?;
module.add_class::<Color>()?;
module.add_class::<PyColorLightSetDeviceInfoParams>()?;
module.add_class::<PyEnergyDataInterval>()?;
module.add_class::<TemperatureUnitKE100>()?;

Ok(())
}
Expand All @@ -74,6 +75,7 @@ fn register_handlers(module: &Bound<'_, PyModule>) -> Result<(), PyErr> {
module.add_class::<PyPlugHandler>()?;

module.add_class::<PyHubHandler>()?;
module.add_class::<PyKE100Handler>()?;
module.add_class::<PyT100Handler>()?;
module.add_class::<PyT110Handler>()?;
module.add_class::<PyT300Handler>()?;
Expand Down
1 change: 1 addition & 0 deletions tapo-py/tapo-py/tapo/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from .power_strip_handler import *
from .power_strip_plug_handler import *
from .requests import *
from .responses import *
from .ke100_handler import *
from .s200b_handler import *
from .t100_handler import *
from .t110_handler import *
Expand Down
29 changes: 28 additions & 1 deletion tapo-py/tapo-py/tapo/hub_handler.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ from tapo.responses import (
T300Result,
T31XResult,
)
from tapo import S200BHandler, T100Handler, T110Handler, T300Handler, T31XHandler
from tapo import KE100Handler, S200BHandler, T100Handler, T110Handler, T300Handler, T31XHandler

class HubHandler:
"""Handler for the [H100](https://www.tapo.com/en/search/?q=H100) hubs."""
Expand Down Expand Up @@ -72,6 +72,33 @@ class HubHandler:
dict: Device info as a dictionary.
"""

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("[email protected]", "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:
Expand Down
69 changes: 69 additions & 0 deletions tapo-py/tapo-py/tapo/ke100_handler.pyi
Original file line number Diff line number Diff line change
@@ -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)
"""
3 changes: 3 additions & 0 deletions tapo-py/tapo-py/tapo/requests/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .color import *
from .energy_data_interval import *
from .set_device_info import *
from tapo.responses import (
TemperatureUnitKE100 as TemperatureUnitKE100,
)
Loading

0 comments on commit 80deac8

Please sign in to comment.