Skip to content

Commit

Permalink
unifi controller to mqtt gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
pdbogen committed Aug 18, 2020
0 parents commit a9c3d9f
Show file tree
Hide file tree
Showing 12 changed files with 675 additions and 0 deletions.
70 changes: 70 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Go
on:
push:
tags: ["v*"]
jobs:
build:
strategy:
matrix:
arch: [linux_amd64, linux_arm, windows_amd64, darwin_amd64]
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.15
uses: actions/setup-go@v1
with:
go-version: 1.15
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: build
run: |
go get -v -t -d ./...
go generate ./...
arch=${{matrix.arch}}
export GOOS=${arch%%_*} GOARCH=${arch##*_} GOARM=5
go build -o unifi2mqtt.$arch -v --ldflags="-s -X'main.version=${{ github.ref }}'" .
- name: upload-artifact unifi2mqtt
uses: actions/upload-artifact@v1
with:
name: unifi2mqtt.${{matrix.arch}}
path: unifi2mqtt.${{matrix.arch}}
release:
name: Release
runs-on: ubuntu-latest
needs: build
steps:
- id: create_release
name: Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
assets:
name: Upload Assets
runs-on: ubuntu-latest
needs: release
strategy:
matrix:
arch: [linux_amd64, linux_arm, windows_amd64, darwin_amd64]
artifact: [unifi2mqtt]
steps:
- name: download ${{matrix.artifact}}.${{matrix.arch}}
uses: actions/download-artifact@v1
with:
name: ${{matrix.artifact}}.${{matrix.arch}}
path: ./
- name: upload ${{matrix.artifact}}.${{matrix.arch}} to release
uses: actions/[email protected]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: ${{matrix.artifact}}.${{matrix.arch}}
asset_name: ${{matrix.artifact}}.${{matrix.arch}}
asset_content_type: application/octet-stream
1 change: 1 addition & 0 deletions README.md
61 changes: 61 additions & 0 deletions cmd/mqtt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cmd

import (
"errors"
"fmt"
"github.com/rs/zerolog"
"net"
"path/filepath"
"strings"
"time"

mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/pdbogen/unifi2mqtt/types"
"github.com/spf13/cobra"
)

func startMqtt(cmd *cobra.Command, ch <-chan types.Client,
log zerolog.Logger) error {
log = log.With().Str("module", "mqtt").Logger()

host, _ := cmd.Flags().GetString("mqtt-host")
prefix, _ := cmd.Flags().GetString("mqtt-prefix")
port, _ := cmd.Flags().GetInt("mqtt-port")
proto, _ := cmd.Flags().GetString("mqtt-proto")

conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return fmt.Errorf("checking MQTT config: %w", err)
}
conn.Close()

opts := mqtt.NewClientOptions()
opts.AddBroker(fmt.Sprintf("%s://%s:%d", proto, host, port))
client := mqtt.NewClient(opts)

log.Info().Msg("connecting to MQTT server...")
token := client.Connect()
token.WaitTimeout(time.Minute)
if err := token.Error(); err != nil {
return fmt.Errorf("mqtt connect failed: %w", err)
}
if !client.IsConnected() {
return errors.New("timeout waiting for mqtt to connect")
}
log.Info().Msg("connected to MQTT")

go func(client mqtt.Client, prefix string, ch <-chan types.Client) {
for device := range ch {
payload := "not_home"
if device.Present {
payload = "home"
}
tok := client.Publish(filepath.Join(prefix, strings.ToLower(device.Name)), 1, true, payload)
if err := tok.Error(); err != nil {
log.Error().Err(err).Msg("error while publishing")
}
}
}(client, prefix, ch)

return nil
}
74 changes: 74 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package cmd

import (
"github.com/rs/zerolog"
"golang.org/x/crypto/ssh/terminal"
"os"
"time"

"github.com/spf13/cobra"
)

var Root = &cobra.Command{
Use: "unifi2mqtt",
Short: "a tool to poll the UniFi controller API and populate clients to mqtt",
Long: "unifi2mqtt is a small gateway daemon in the spirit of " +
"zwave2mqtt, which polls the UniFi Controller API and feeds " +
"information about connected devices into MQTT, for the intended " +
"purpose of powering HomeAssistant's MQTT Device Tracker.",
Run: rootRun,
}

func init() {
Root.Flags().String("host", "localhost", "hostname of the UniFi "+
"controller")
Root.Flags().Int("port", 8443, "UniFi controller port")
Root.Flags().Bool("verify-tls", false, "if true, verify the TLS "+
"certificate of the UniFi controller")
Root.Flags().String("username", "", "login user on UniFi controller "+
"(env: UNIFI2MQTT_USER)")
Root.Flags().String("password", "", "user password on UniFi controller "+
"(env: UNIFI2MQTT_PASS)")
Root.Flags().StringSlice("include-name", nil, "client names to include; "+
"if no --include flags are set, all clients will be reported. If a "+
"name is surrounded with `/`, it's interpreted as a regular "+
"expression. Flag may be specified multiple times, or just once "+
"with `,`-separated options.")
Root.Flags().Duration("unifi-timeout", time.Minute, "how long does a "+
"device need to be not 'seen' before it's repoted as not_home. Note "+
"that since unifi2mqtt _polls_ the unifi API once a minute, there's "+
"no point in setting this lower than that. Unifi itself will stop "+
"reporting a device after about five minutes.")

Root.Flags().String("mqtt-host", "localhost", "hostname of the MQTT server to publish to")
Root.Flags().String("mqtt-prefix", "unifi", "a prefix for the mqtt "+
"space to publish, messages are published to "+
"{mqtt-prefix}/{device-name}")
Root.Flags().Int("mqtt-port", 1883, "port used to connect to MQTT")
Root.Flags().String("mqtt-proto", "tcp", "protocol used to connect to MQTT; options are `tcp`, `ssl`, `ws`")
}

func rootRun(cmd *cobra.Command, _ []string) {
var log zerolog.Logger

if terminal.IsTerminal(int(os.Stdout.Fd())) {
log = zerolog.New(zerolog.NewConsoleWriter())
} else {
log = zerolog.New(os.Stdout)
}
log = log.With().Timestamp().Logger()

log.Info().Str("version", cmd.Root().Version).Msg("unifi2mqtt starting...")

ch, err := startUnifi(cmd, log)
if err != nil {
log.Fatal().Err(err).Msg("could not connect to unifi controller")
}

if err := startMqtt(cmd, ch, log); err != nil {
log.Fatal().Err(err).Msg("could not start mqtt client")
}

// block forever
select {}
}
49 changes: 49 additions & 0 deletions cmd/unifi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"fmt"
"github.com/rs/zerolog"
"os"

"github.com/pdbogen/unifi2mqtt/types"
"github.com/spf13/cobra"
)

func startUnifi(cmd *cobra.Command, log zerolog.Logger) (<-chan types.Client, error) {
log = log.With().Str("module", "unifi").Logger()

verifyFlags(cmd, log)

names, _ := cmd.Flags().GetStringSlice("include-name")
matchers, err := types.NewMatchers(names)
if err != nil {
return nil, fmt.Errorf("parsing --include-name: %w", err)
}

username, _ := cmd.Flags().GetString("username")
password, _ := cmd.Flags().GetString("password")
host, _ := cmd.Flags().GetString("host")
port, _ := cmd.Flags().GetInt("port")
verifyTls, _ := cmd.Flags().GetBool("verify-tls")
deviceTimeout, _ := cmd.Flags().GetDuration("unifi-timeout")

u, err := types.NewUnifi(username, password, host, port, verifyTls, deviceTimeout, matchers, log)
if err != nil {
return nil, fmt.Errorf("starting unifi client: %w", err)
}

return u.Chan(), nil
}

func verifyFlags(cmd *cobra.Command, log zerolog.Logger) {
for flag, env := range map[string]string{"username": "UNIFI2MQTT_USER", "password": "UNIFI2MQTT_PASS"} {
flagV, _ := cmd.Flags().GetString(flag)
if flagV == "" {
flagV = os.Getenv(env)
}
if flagV == "" {
log.Fatal().Msgf("flag --%s or environment variable %s is required", flag, env)
}
cmd.Flags().Set(flag, flagV)
}
}
17 changes: 17 additions & 0 deletions doc/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"log"
"os"

"github.com/pdbogen/unifi2mqtt/cmd"
"github.com/spf13/cobra/doc"
)

func main() {
_ = os.Mkdir("./docs", os.FileMode(0755))
err := doc.GenMarkdownTree(cmd.Root, "./docs/")
if err != nil {
log.Fatal(err)
}
}
30 changes: 30 additions & 0 deletions docs/unifi2mqtt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## unifi2mqtt

a tool to poll the UniFi controller API and populate clients to mqtt

### Synopsis

unifi2mqtt is a small gateway daemon in the spirit of zwave2mqtt, which polls the UniFi Controller API and feeds information about connected devices into MQTT, for the intended purpose of powering HomeAssistant's MQTT Device Tracker.

```
unifi2mqtt [flags]
```

### Options

```
-h, --help help for unifi2mqtt
--host string hostname of the UniFi controller (default "localhost")
--include-name / client names to include; if no --include flags are set, all clients will be reported. If a name is surrounded with /, it's interpreted as a regular expression. Flag may be specified multiple times, or just once with `,`-separated options.
--mqtt-host string hostname of the MQTT server to publish to (default "localhost")
--mqtt-port int port used to connect to MQTT (default 1883)
--mqtt-prefix string a prefix for the mqtt space to publish, messages are published to {mqtt-prefix}/{device-name} (default "unifi")
--mqtt-proto tcp protocol used to connect to MQTT; options are tcp, `ssl`, `ws` (default "tcp")
--password string user password on UniFi controller (env: UNIFI2MQTT_PASS)
--port int UniFi controller port (default 8443)
--unifi-timeout duration how long does a device need to be not 'seen' before it's repoted as not_home. Note that since unifi2mqtt _polls_ the unifi API once a minute, there's no point in setting this lower than that. Unifi itself will stop reporting a device after about five minutes. (default 1m0s)
--username string login user on UniFi controller (env: UNIFI2MQTT_USER)
--verify-tls if true, verify the TLS certificate of the UniFi controller
```

###### Auto generated by spf13/cobra on 17-Aug-2020
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/pdbogen/unifi2mqtt

go 1.14

require (
github.com/eclipse/paho.mqtt.golang v1.2.0
github.com/rs/zerolog v1.19.0
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golift.io/unifi v4.1.6+incompatible
)
Loading

0 comments on commit a9c3d9f

Please sign in to comment.