Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 9e65407

Browse files
NickCaogithub-actions[bot]
authored andcommitted
Implement TasmotaPower driver
(cherry picked from commit 6f40cfe)
1 parent 872d22f commit 9e65407

10 files changed

Lines changed: 305 additions & 0 deletions

File tree

docs/source/reference/package-apis/drivers/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Drivers that control the power state and basic operation of devices:
2222
* **[DUT Link](dutlink.md)** (`jumpstarter-driver-dutlink`) - [DUT Link
2323
Board](https://github.com/jumpstarter-dev/dutlink-board) hardware control
2424
* **[Energenie PDU](energenie.md)** (`jumpstarter-driver-energenie`) - Energenie PDUs
25+
* **[Tasmota](tasmota.md)** (`jumpstarter-driver-tasmota`) - Tasmota hardware control
2526

2627
### Communication Drivers
2728

@@ -93,6 +94,7 @@ raspberrypi.md
9394
sdwire.md
9495
shell.md
9596
snmp.md
97+
tasmota.md
9698
tftp.md
9799
uboot.md
98100
ustreamer.md
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../../packages/jumpstarter-driver-tasmota/README.md
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Tasmota driver
2+
3+
`jumpstarter-driver-tasmota` provides functionality for interacting with tasmota compatible devices.
4+
5+
## Installation
6+
7+
```shell
8+
pip3 install --extra-index-url https://pkg.jumpstarter.dev/simple/ jumpstarter-driver-tasmota
9+
```
10+
11+
## Configuration
12+
13+
Example configuration:
14+
15+
```yaml
16+
export:
17+
power:
18+
type: jumpstarter_driver_tasmota.driver.TasmotaPower
19+
```
20+
21+
### Config parameters
22+
23+
| Parameter | Description | Default |
24+
|--------------|-----------------------------------------------------------------|----------|
25+
| `host` | MQTT broker hostname or IP address | Required |
26+
| `port` | MQTT broker port | 1883 |
27+
| `tls` | MQTT broker TLS enabled | True |
28+
| `client_id` | Client identifier for MQTT connection | |
29+
| `transport` | Transport protocol, one of "tcp", "websockets", "unix" | "tcp" |
30+
| `timeout` | Timeout in seconds for operations | |
31+
| `username` | Username for MQTT authentication | |
32+
| `password` | Password for MQTT authentication | |
33+
| `cmnd_topic` | MQTT topic for sending commands to the Tasmota device | Required |
34+
| `stat_topic` | MQTT topic for receiving status updates from the Tasmota device | Required |
35+
36+
## API Reference
37+
38+
The tasmota power driver provides a `PowerClient` with the following API:
39+
40+
```{eval-rst}
41+
.. autoclass:: jumpstarter_driver_power.client.PowerClient()
42+
:no-index:
43+
:members: on, off
44+
```

packages/jumpstarter-driver-tasmota/jumpstarter_driver_tasmota/__init__.py

Whitespace-only changes.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from dataclasses import dataclass, field
2+
from threading import Condition
3+
from typing import Literal
4+
5+
import paho.mqtt.client as paho
6+
from jumpstarter_driver_power.driver import PowerInterface
7+
from paho.mqtt.enums import CallbackAPIVersion
8+
9+
from jumpstarter.driver import Driver, export
10+
11+
12+
@dataclass(kw_only=True)
13+
class TasmotaPower(PowerInterface, Driver):
14+
"""driver for tasmota compatible power switches"""
15+
16+
client_id: str | None = None
17+
transport: Literal["tcp", "websockets", "unix"] = "tcp"
18+
timeout: float | None = None
19+
20+
host: str
21+
port: int = 1883
22+
tls: bool = True
23+
24+
username: str | None = None
25+
password: str | None = None
26+
27+
cmnd_topic: str
28+
stat_topic: str
29+
30+
mq: paho.Client = field(init=False)
31+
state: str | None = field(init=False, default=None)
32+
cond: Condition = field(init=False, default_factory=Condition)
33+
34+
def __post_init__(self):
35+
if hasattr(super(), "__post_init__"):
36+
super().__post_init__()
37+
38+
self.mq = paho.Client(
39+
callback_api_version=CallbackAPIVersion.VERSION2,
40+
client_id=self.client_id,
41+
transport=self.transport,
42+
)
43+
44+
def on_message(client, userdata, msg):
45+
if msg.topic == self.stat_topic:
46+
self.state = msg.payload.decode()
47+
with self.cond:
48+
self.cond.notify_all()
49+
50+
self.mq.on_message = on_message
51+
52+
if self.tls:
53+
self.mq.tls_set()
54+
55+
self.mq.username_pw_set(self.username, self.password)
56+
self.mq.connect(self.host, self.port)
57+
self.mq.loop_start()
58+
59+
self.mq.subscribe(self.stat_topic)
60+
61+
def publish(self, state):
62+
self.mq.publish(
63+
self.cmnd_topic,
64+
payload=state,
65+
qos=1,
66+
).wait_for_publish(
67+
timeout=self.timeout,
68+
)
69+
with self.cond:
70+
self.cond.wait_for(
71+
lambda: self.state == state,
72+
timeout=self.timeout,
73+
)
74+
75+
@export
76+
def on(self):
77+
self.publish("ON")
78+
79+
@export
80+
def off(self):
81+
self.publish("OFF")
82+
83+
@export
84+
def read(self):
85+
pass
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
from pytest_mqtt.model import MqttMessage
3+
4+
from .driver import TasmotaPower
5+
from jumpstarter.common.utils import serve
6+
7+
8+
@pytest.mark.skip("requires docker")
9+
def test_tasmota_power(mosquitto, capmqtt):
10+
cmnd_topic = "cmnd/tasmota_6990F2/POWER"
11+
stat_topic = "stat/tasmota_6990F2/POWER"
12+
13+
with serve(
14+
TasmotaPower(
15+
host=mosquitto[0],
16+
port=int(mosquitto[1]),
17+
tls=False,
18+
transport="tcp",
19+
cmnd_topic=cmnd_topic,
20+
stat_topic=stat_topic,
21+
)
22+
) as client:
23+
capmqtt.publish(topic=stat_topic, payload="ON")
24+
client.on()
25+
assert MqttMessage(topic=cmnd_topic, payload=b"ON", userdata=None) in capmqtt.messages
26+
27+
capmqtt.publish(topic=stat_topic, payload="OFF")
28+
client.off()
29+
assert MqttMessage(topic=cmnd_topic, payload=b"OFF", userdata=None) in capmqtt.messages

packages/jumpstarter-driver-tasmota/jumpstarter_driver_tasmota/py.typed

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[project]
2+
name = "jumpstarter-driver-tasmota"
3+
dynamic = ["version", "urls"]
4+
description = "Jumpstarter driver for controlling Tasmota-compatible devices via MQTT"
5+
readme = "README.md"
6+
license = "Apache-2.0"
7+
authors = [{ name = "Nick Cao", email = "nickcao@nichi.co" }]
8+
requires-python = ">=3.11"
9+
dependencies = [
10+
"anyio>=4.6.2.post1",
11+
"jumpstarter_driver_power",
12+
"jumpstarter",
13+
"paho-mqtt>=2.1.0",
14+
]
15+
16+
[project.entry-points."jumpstarter.drivers"]
17+
TasmotaPower = "jumpstarter_driver_tasmota.driver:TasmotaPower"
18+
19+
20+
[tool.hatch.version]
21+
source = "vcs"
22+
raw-options = { 'root' = '../../' }
23+
24+
[tool.hatch.metadata.hooks.vcs.urls]
25+
Homepage = "https://jumpstarter.dev"
26+
source_archive = "https://github.com/jumpstarter-dev/repo/archive/{commit_hash}.zip"
27+
28+
[tool.pytest.ini_options]
29+
addopts = "--cov --cov-report=html --cov-report=xml"
30+
log_cli = true
31+
log_cli_level = "INFO"
32+
testpaths = ["jumpstarter_driver_tasmota"]
33+
34+
[build-system]
35+
requires = ["hatchling", "hatch-vcs", "hatch-pin-jumpstarter"]
36+
build-backend = "hatchling.build"
37+
38+
[tool.hatch.build.hooks.pin_jumpstarter]
39+
name = "pin_jumpstarter"
40+
41+
[dependency-groups]
42+
dev = [
43+
"pytest-cov>=6.0.0",
44+
"pytest>=8.3.3",
45+
"pytest-mqtt>=0.5.0",
46+
]

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ jumpstarter-driver-probe-rs = { workspace = true }
2222
jumpstarter-driver-pyserial = { workspace = true }
2323
jumpstarter-driver-qemu = { workspace = true }
2424
jumpstarter-driver-sdwire = { workspace = true }
25+
jumpstarter-driver-tasmota = { workspace = true }
2526
jumpstarter-driver-tftp = { workspace = true }
2627
jumpstarter-driver-snmp = { workspace = true }
2728
jumpstarter-driver-shell = { workspace = true }
@@ -75,6 +76,7 @@ locale = "en-us"
7576
[tool.typos.default.extend-words]
7677
ser = "ser"
7778
Pn = "Pn"
79+
mosquitto = "mosquitto"
7880

7981
[tool.coverage.run]
8082
omit = ["conftest.py", "test_*.py", "*_test.py", "*_pb2.py", "*_pb2_grpc.py"]

uv.lock

Lines changed: 96 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)