Skip to content

Commit

Permalink
chore: wrote README, created beta workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
WhySoBad committed Mar 28, 2024
1 parent 30514a0 commit 9da314d
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 72 deletions.
43 changes: 43 additions & 0 deletions .github/workflows/publish-beta.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Publish beta docker image

on:
workflow_dispatch:

jobs:
build-and-publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout source
uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up docker buildx
uses: docker/setup-buildx-action@v3

- name: Log in to the container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata from event
uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
provenance: false
tags: ghcr.io/${{ github.repository }}:beta
labels: ${{ steps.meta.outputs.labels }}
3 changes: 2 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata from event
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
Expand All @@ -41,5 +41,6 @@ jobs:
context: .
platforms: linux/amd64,linux/arm64
push: true
provenance: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
141 changes: 135 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,144 @@
# tapoctl

Control your tapo light bulbs from the command line on your local network
Control your tapo light bulbs from anywhere on your local network using the command line

## Motivation and idea

I wanted to control my tapo light bulbs from my local network but without the necessity of granting them access to my local network (or to the internet at all).
Additionally, I wanted to be able to control by light bulbs from the command line.

The idea behind `tapoctl` is to host the included gRPC server on a local device (e.g. a raspberry pi) which is connected to the local and the iot network.
It acts as a proxy and allows you to control your light bulbs from anywhere on your local network.
The idea behind this project is to create a gRPC [server](#server) which can be hosted on a local device (see [example setup](/docs/example.md)) and which is connected
to both your local network and the network containing the light bulbs. It then acts as a proxy and allows you to control
your light bulbs from anywhere on your local network without using the proprietary app.

Additionally, should the cli not meet your needs the usage of protocol buffers
allows to quickly create your own client for your needs.
>[!NOTE]
> You're still able to use the proprietary app if wanted as long as your smartphone is connected to the same wifi network as your light bulbs are
[TODO: Write docs at /docs]
Additionally, should the cli not meet your needs the use of protocol buffers allows a quick client implementation in any language.

## Features

* Cli to control your tapo light bulbs
* Offline control for your tapo light bulbs
* gRPC server which allows easy client integration

## Supported devices

Currently, the following light bulbs are supported:

* L530
* L630
* L900

For the following devices the support is coming soon™:

* L510
* L520
* L610
* Generic light bulbs with limited feature set

## Cli

The cli supports the following commands:

* `devices`: List all devices registered on the server
* `set <device>`: Update one or more properties of a light bulb <br>
`--brightness`: Brightness value between 1 and 100<br>
`--hue`: Hue value between 1 and 360<br>
`--saturation`: Saturation value between 1 and 100<br>
`--temperature`: Set color temperature to value between 2500K and 6500K<br>
`--color`: Set predefined google home color<br>
`--power`: Boolean whether to turn the lamp on/off
* `info <device>`: Print current state of a light bulb
* `usage <device>`: Print energy and time usage information for a light bulb
* `on <device>`: Turn light bulb on
* `off <device>`: Turn light bulb off
* `reset <device>`: Reset light bulb to factory defaults
* `serve`: Start the gRPC server. More about this can be read in [the server section](#server) <br>
`--port`: Port on which the server should listen

Additionally, there are some global arguments which work on all commands:
* `--config`: Path to the configuration file which should be used
* `--json`: Print the response from the server as json should there be one
* `--address`: Address used for connecting to the gRPC server
* `--port`: Port used for connecting to the gRPC server
* `--secure`: Use https instead of http to connect to the gRPC server

### Configuration

By default, the configuration file is expected to be at `$HOME/.config/tapoctl/config.toml`. There are two different configuration formats: the **client** and the **server** configuration.

The client configuration is used to persist options for connecting to a server whilst the server configuration is used to register devices on the server. The server configuration is documented [in the server section](#configuration-1) in detail.

The following configuration is an example of a **client** configuration:
```toml
# Connect to the server located at `10.10.10.10`
address="10.10.10.10"
# Connect on port `19991` instead of default `19191`
port=19991
# Use http as communication protocol
secure=false
```

The client configuration is optional and when not specified otherwise everything falls back to default values

## Server

The binary includes a gRPC server which can be started using `serve` command.

The server has to be in the same network as the devices. Should your devices be in another network than your local network (e.g. guest or iot network)
you'll have to make sure the device on which the server is hosted is connected to both your local network and the network in which the lamps are located in order for you to be
able to control the bulbs from your local network. More about this can be seen in the [example setup](/docs/example.md) with a Raspberry Pi.

### Configuration

Unlike the client configuration the server configuration is required for a server to be started. It contains your tapo account credentials. Those are needed for
constructing the secret needed for interacting with the lamps. Under no circumstances are those credentials used to connect to any (remote) tapo servers.

Additionally, you need to register your devices which then will be accessible through the gRPC api.

```toml
# Your tapo account credentials
[auth]
username=""
password=""

# Register a device with the name `lamp-1`
[devices.lamp-1]
type="L530" # The device type of the light bulb (L530, L520, ...)
address="10.255.255.10" # The address under which the device can be reached

port=19191 # Optional port to listen on. Default: 19191
```

>[!TIP]
> You can find the ip address of your device in the official tapo app or through a
> [arp scan](https://linux.die.net/man/1/arp-scan) on the network your device is on
>
> When you use something like a pihole and assigned a hostname to your device you can also specify the hostname
> in the `address` field
### Using docker

The server can easily be set up using the `ghcr.io/whysobad/tapoctl` docker image. Since you want to communicate with your light bulbs over the network
the container needs host network access:

```bash
docker run \
--network host \
--volume ./config.toml:/home/tapo/.config/tapoctl/config.toml \
ghcr.io/whysobad/tapoctl
```

When using docker compose the following configuration can be used:

```yaml
version: '3.8'

services:
tapoctl:
image: ghcr.io/whysobad/tapoctl
network_mode: host
volumes:
- ./config:/home/tapo/.config/tapoctl/config.toml
```
1 change: 0 additions & 1 deletion docs/cli.md

This file was deleted.

5 changes: 3 additions & 2 deletions docs/example.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Example setup

In this section I'll describe my tapoctl setup using a raspberry pi 4b.
In this section I'll describe my tapoctl setup using a Raspberry Pi 4b.

[TODO]
>[!NOTE]
> The example setup will be documented soon
53 changes: 0 additions & 53 deletions docs/server.md

This file was deleted.

14 changes: 7 additions & 7 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ pub struct Cli {
pub address: Option<String>,

/// Port for client to connect to gRPC server [default: config or 19191]
#[arg(long, short, value_parser = clap::value_parser!(u16).range(1..=65535), global = true)]
#[arg(long, short = 'n', value_parser = clap::value_parser!(u16).range(1..=65535), global = true)]
pub port: Option<u16>,

/// Boolean whether to connect to the gRPC using https [default: config or false]
#[arg(long, short, global = true)]
#[arg(long, short = 'i', global = true)]
pub secure: Option<bool>,

/// Print result (if any) as json
Expand Down Expand Up @@ -59,19 +59,19 @@ pub enum ClientCommand {
/// Device which should be updated
device: String,

/// New brightness value
/// Brightness value between 1 and 100
#[arg(value_parser = parse_100_value, allow_negative_numbers = true, long, short)]
brightness: Option<IntegerValueChange>,

#[command(flatten)]
hue_saturation: HueSaturation,

/// New color temperature
/// Color temperature in kelvin between 2500 and 6500
#[arg(value_parser = parse_kelvin_value, allow_negative_numbers = true, long, short)]
temperature: Option<IntegerValueChange>,

/// Use predefined google home color
#[arg(long, short, value_enum)]
#[arg(long, short = 'o', value_enum)]
color: Option<Color>,

/// Turn device on or off
Expand Down Expand Up @@ -108,11 +108,11 @@ pub enum ClientCommand {
#[derive(Args, Clone, Debug)]
#[group(multiple = true, requires_all = ["hue", "saturation"])]
pub struct HueSaturation {
/// New hue value
/// Hue value between 1 and 360
#[arg(value_parser = parse_360_value, long, short = 'u', allow_negative_numbers = true)]
pub hue: Option<IntegerValueChange>,

/// New saturation value
/// Saturation value between 1 and 100
#[arg(value_parser = parse_100_value, long, short, allow_negative_numbers = true)]
pub saturation: Option<IntegerValueChange>,
}
Expand Down
2 changes: 1 addition & 1 deletion src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl Device {
client.l520(&definition.address).await.ok().map(DeviceHandler::Light)
}
SupportedDevice::L610 => {
client.l520(&definition.address).await.ok().map(DeviceHandler::Light)
client.l610(&definition.address).await.ok().map(DeviceHandler::Light)
}
SupportedDevice::Generic => {
client.generic_device(&definition.address).await.ok().map(DeviceHandler::Generic)
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ async fn main() -> anyhow::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

let cli: Cli = Cli::parse();
let mut config = cli.config;
let config = cli.config;
let json = cli.json;

match cli.command {
Expand Down

0 comments on commit 9da314d

Please sign in to comment.