Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ble support #2

Merged
merged 2 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 95 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ shellyctl is an unofficial command line client for the [Shelly Gen2 API](https:/

## Features
* mDNS discovery of shelly devices on the local network.
* Bluetooth Low Energy (BLE) discovery of shelly devices for RPC, monitoring, and initial setup.
* Command line interface for documented APIs.
* prometheus metrics endpoint with the status of known devices.

## Maturity
This library is currently in active development (as of December 2023). It has meaningful gaps in testing and functionality. At this stage there is no guarantee of backwards compatibility. Once the project reaches a stable state, I will begin crafting releases with semantic versioning.
This library is currently in active development (as of January 2024). It has meaningful gaps in testing and functionality. At this stage there is no guarantee of backwards compatibility. Once the project reaches a stable state, I will begin tagging releases with semantic versioning.

## Usage
```
Expand Down Expand Up @@ -77,9 +79,99 @@ Global Flags:
--log-level string threshold for outputing logs: trace, debug, info, warn, error, fatal, panic (default "warn")
```

### RPC Command-line

#### Example
```
$ shellyctl switch set-config --ble-device=D4:D4:DA:09:2E:B6 --auto-off=true --auto-off-delay=60

Response to Switch.SetConfig command:
Restart Required: false

$ shellyctl switch set --ble-device=D4:D4:DA:09:2E:B6 --on=true

Response to Switch.Set command:
Was On: true
```

#### Menu Heirarchy
- `ble`
- `get-config` ([BLE.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/BLE/#blegetconfig))
- `get-status` ([BLE.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/BLE/#blegetstatus))
- `set-config` ([BLE.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/BLE/#blesetconfig))
- `cloud`
- `get-config` ([Cloud.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cloud/#cloudgetconfig))
- `get-status` ([Cloud.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cloud/#cloudgetstatus))
- `set-config` ([Cloud.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cloud/#cloudsetconfig))
- `cover`
- `calibrate` ([Cover.Calibrate](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#covercalibrate))
- `close` ([Cover.Close](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#coverclose))
- `get-config` ([Cover.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#covergetconfig))
- `get-status` ([Cover.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#covergetstatus))
- `go-to-position` ([Cover.GoToPosition](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#covergotoposition))
- `open` ([Cover.Open](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#coveropen))
- `reset-counters` ([Cover.ResetCounters](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#coverresetcounters))
- `set-config` ([Cover.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#coversetconfig))
- `stop` ([Cover.Stop](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Cover/#coverstop))
- input
- `check-expression` ([Input.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Input#inputcheckexpression))
- `get-config` ([Input.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Input#inputsetconfig))
- `get-status` ([Input.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Input#inputgetstatus))
- `set-config` ([Input.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Input#inputsetconfig))
- `light`
- `get-config` ([Light.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Light#lightgetconfig))
- `get-status` ([Light.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Light#lightgetstatus))
- `set` ([Light.Set](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Light#lightset))
- `set-config` ([Light.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Light#lightsetconfig))
- `toggle` ([Light.Toggle](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Light#lighttoggle))
- `mqtt`
- `get-config` ([MQTT.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Mqtt#mqttgetconfig))
- `get-status` ([MQTT.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Mqtt#mqttgetstatus))
- `set-config` ([MQTT.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Mqtt#mqttsetconfig))
- `schedule`
- `delete` ([Schedule.Delete](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Schedule#scheduledelete))
- `delete-all` ([Schedule.DeleteAll](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Schedule#scheduledeleteall))
- `shelly`
- `check-for-update` ([Shelly.CheckForUpdate](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Shelly#shellycheckforupdate))
- `get-config` ([Shelly.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Shelly#shellygetconfig))
- `get-status` ([Shelly.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Shelly#shellygetstatus))
- `reboot` ([Shelly.Reboot](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Shelly#shellyreboot))
- `set-auth` ([Shelly.SetAuth](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Shelly#shellysetauth))
- `switch`
- `get-config` ([Switch.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#switchgetconfig))
- `get-status` ([Switch.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#switchgetstatusg))
- `set` ([Switch.Set](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#switchset))
- `set-config` ([Switch.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#switchsetconfig))
- `toggle` ([Switch.Toggle](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#switchtoggle))
- `sys`
- `get-config` ([Sys.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Sys#sysgetconfig))
- `get-status` ([Sys.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Sys#sysgetstatus))
- `set-config` ([Sys.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Sys#syssetconfig))
- `wifi`
- `get-config` ([WiFi.GetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/WiFi#wifigetconfig))
- `get-status` ([WiFi.GetStatus](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/WiFi#wifigetstatus))
- `set-config` ([WiFi.SetConfig](https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/WiFi#wifisetconfig))


### Device Initial Setup
By default Shelly devices can be configured with RPCs over Bluetooth Low Energy (BLE) channel. The initial configuration is therefore just a matter of configuring network connectivity, optionally disabling BLE, and optionally setting authentication.
```
$ shellyctl wifi set-config --sta-enable=true --sta-ssid=INTERNET --sta-pass=password --ble-device=AA:BB:CC:DD:EE:FF

Response to Wifi.SetConfig command:
Restart Required: false

$ shellyctl ble set-config --enable=false --host=192.168.1.62

Response to BLE.SetConfig command:
Restart Required: true

$ shellyctl shelly reboot --host=192.168.1.62

Response to Shelly.Reboot command:
```

## TODO
* BLE Support
* Device Provisioning
* Device Backup & Restore / Support for configuration as code style provisioning.
* MQTT / WebSocket support

Expand Down
2 changes: 1 addition & 1 deletion cmd/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func init() {
}

baggage.Discoverer = discovery.NewDiscoverer(dOpts...)
if err := discoveryAddHosts(ctx, baggage.Discoverer); err != nil {
if err := discoveryAddDevices(ctx, baggage.Discoverer); err != nil {
l.Fatal().Err(err).Msg("adding devices")
}
return childRun(cmd, args)
Expand Down
51 changes: 49 additions & 2 deletions cmd/helper_discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
var (
hosts []string
mdnsSearch bool
bleSearch bool
bleDevices []string
mdnsInterface string
mdnsZone string
mdnsService string
Expand All @@ -40,6 +42,18 @@ func discoveryFlags(f *pflag.FlagSet, withTTL bool) {
false,
"if true, devices will be discovered via mDNS")

f.BoolVar(
&bleSearch,
"ble-search",
false,
"if true, devices will be discovered via Bluetooth Low-Energy")

f.StringArrayVar(
&bleDevices,
"ble-device",
[]string{},
"MAC address of a single bluetooth low-energy device. May be specified multiple times to work with multiple devices.")

f.StringVar(
&mdnsInterface,
"mdns-interface",
Expand Down Expand Up @@ -96,7 +110,7 @@ func discoveryFlags(f *pflag.FlagSet, withTTL bool) {
}

func discoveryOptionsFromFlags() (opts []discovery.DiscovererOption, err error) {
if len(hosts) == 0 && !mdnsSearch {
if len(hosts) == 0 && len(bleDevices) == 0 && !mdnsSearch && !bleSearch {
return nil, errors.New("no hosts and or discovery (mDNS)")
}
if mdnsInterface != "" {
Expand Down Expand Up @@ -125,12 +139,27 @@ func discoveryOptionsFromFlags() (opts []discovery.DiscovererOption, err error)
return opts, err
}

func discoveryAddHosts(ctx context.Context, d *discovery.Discoverer) error {
func discoveryAddDevices(ctx context.Context, d *discovery.Discoverer) error {
l := log.Ctx(ctx)
var wg sync.WaitGroup
concurrencyLimit := make(chan struct{}, discoveryConcurrency)
defer close(concurrencyLimit)
defer wg.Wait()
if len(bleDevices) > 0 {
select {
case concurrencyLimit <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
}
wg.Add(1)
go func() {
defer func() {
wg.Done()
<-concurrencyLimit
}()
discoveryAddBLEDevices(ctx, d)
}()
}
for _, h := range hosts {
// This chan send will block if the we exceed discoveryConcurrency.
select {
Expand All @@ -155,3 +184,21 @@ func discoveryAddHosts(ctx context.Context, d *discovery.Discoverer) error {
}
return nil
}

func discoveryAddBLEDevices(ctx context.Context, d *discovery.Discoverer) error {
l := log.Ctx(ctx)
for _, mac := range bleDevices {
if err := ctx.Err(); err != nil {
return err
}
_, err := d.AddBLE(ctx, mac)
if err == nil {
continue
}
if !skipFailedHosts {
l.Fatal().Err(err).Msg("adding device")
}
l.Warn().Err(err).Msg("adding device; continuing because `skip-failed-hosts=true`")
}
return nil
}
2 changes: 1 addition & 1 deletion cmd/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ var prometheusCmd = &cobra.Command{
l.Fatal().Err(err).Msg("parsing flags")
}
disc := discovery.NewDiscoverer(dOpts...)
if err := discoveryAddHosts(ctx, disc); err != nil {
if err := discoveryAddDevices(ctx, disc); err != nil {
l.Fatal().Err(err).Msg("adding devices")
}

Expand Down
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.21.3
require (
github.com/go-logr/zerologr v1.2.3
github.com/hashicorp/mdns v1.0.5
github.com/jcodybaker/go-shelly v0.0.0-20240120015947-a59697f0c673
github.com/jcodybaker/go-shelly v0.0.0-20240120171830-b7e86393c146
github.com/mongoose-os/mos v0.0.0-20230313140341-b44964e63a92
github.com/rs/zerolog v1.31.0
github.com/spf13/cobra v1.8.0
Expand All @@ -24,8 +24,11 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand All @@ -43,6 +46,7 @@ require (
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/miekg/dns v1.1.41 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muka/go-bluetooth v0.0.0-20221213043340-85dc80edc4e1 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
Expand All @@ -51,13 +55,16 @@ require (
github.com/prometheus/procfs v0.12.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/saltosystems/winrt-go v0.0.0-20230921082907-2ab5b7d431e1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinygo-org/cbgo v0.0.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
Expand All @@ -79,4 +86,5 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
tinygo.org/x/bluetooth v0.8.0 // indirect
)
Loading
Loading