2024-08-19 19:43:17 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-10-16 07:42:34 -06:00
|
|
|
"cmp"
|
2024-09-22 10:23:57 -06:00
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
2024-08-19 19:43:17 -06:00
|
|
|
"fmt"
|
2024-08-23 20:42:03 -06:00
|
|
|
"io"
|
2024-08-19 19:43:17 -06:00
|
|
|
"log"
|
2024-09-22 10:23:57 -06:00
|
|
|
"net/http"
|
2024-10-16 07:42:34 -06:00
|
|
|
"slices"
|
|
|
|
"strings"
|
2024-09-22 10:23:57 -06:00
|
|
|
"time"
|
2024-08-19 19:43:17 -06:00
|
|
|
|
|
|
|
"fyne.io/fyne/v2"
|
|
|
|
"fyne.io/fyne/v2/app"
|
|
|
|
"fyne.io/fyne/v2/container"
|
2024-08-23 20:42:03 -06:00
|
|
|
"fyne.io/fyne/v2/dialog"
|
2024-08-19 19:43:17 -06:00
|
|
|
"fyne.io/fyne/v2/driver/desktop"
|
2024-08-23 20:42:03 -06:00
|
|
|
"fyne.io/fyne/v2/storage"
|
2024-08-19 19:43:17 -06:00
|
|
|
"fyne.io/fyne/v2/theme"
|
|
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
"github.com/pawal/go-hass"
|
|
|
|
)
|
|
|
|
|
2024-10-16 07:42:34 -06:00
|
|
|
func sortEntries(entries *hass.States) {
|
|
|
|
slices.SortFunc(*entries, func(a hass.State, b hass.State) int {
|
|
|
|
an := fmt.Sprintf("%s", a.Attributes["friendly_name"])
|
|
|
|
bn := fmt.Sprintf("%s", b.Attributes["friendly_name"])
|
|
|
|
|
|
|
|
if n := strings.Compare(an, bn); n != 0 {
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
return cmp.Compare(a.EntityID, b.EntityID)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-08-23 20:42:03 -06:00
|
|
|
func loadData(h *hass.Access, lightCards *[]fyne.CanvasObject, switchCards *[]fyne.CanvasObject) {
|
|
|
|
lights, err := h.FilterStates("light")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalln(err)
|
|
|
|
}
|
|
|
|
switches, err := h.FilterStates("switch")
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalln(err)
|
|
|
|
}
|
2024-08-19 19:43:17 -06:00
|
|
|
|
2024-10-16 07:42:34 -06:00
|
|
|
sortEntries(&lights)
|
|
|
|
sortEntries(&switches)
|
|
|
|
|
2024-08-23 20:42:03 -06:00
|
|
|
for entity := range lights {
|
|
|
|
e := lights[entity]
|
|
|
|
card := makeEntity(e, h)
|
2024-09-25 08:06:18 -06:00
|
|
|
if card != nil {
|
|
|
|
*lightCards = append(*lightCards, card)
|
|
|
|
}
|
2024-08-23 20:42:03 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
for entity := range switches {
|
|
|
|
e := switches[entity]
|
|
|
|
card := makeEntity(e, h)
|
2024-09-25 08:06:18 -06:00
|
|
|
if card != nil {
|
|
|
|
*switchCards = append(*switchCards, card)
|
|
|
|
}
|
2024-08-23 20:42:03 -06:00
|
|
|
}
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func getDevice(id string, h *hass.Access) (hass.Device, error) {
|
|
|
|
s, err := h.GetState(id)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return h.GetDevice(s)
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeEntity(e hass.State, h *hass.Access) *widget.Card {
|
2024-09-25 08:06:18 -06:00
|
|
|
if e.State != "on" && e.State != "off" {
|
|
|
|
return nil
|
|
|
|
}
|
2024-08-19 19:43:17 -06:00
|
|
|
fmt.Printf("%s: (%s) %s\n", e.EntityID,
|
|
|
|
e.Attributes["friendly_name"],
|
|
|
|
e.State)
|
|
|
|
|
|
|
|
entityName := fmt.Sprintf("%s", e.Attributes["friendly_name"])
|
|
|
|
entityButton := NewToggle()
|
|
|
|
if e.State == "on" {
|
|
|
|
entityButton.On = true
|
|
|
|
}
|
|
|
|
entityButton.OnChanged(func(on bool) {
|
|
|
|
dev, err := getDevice(e.EntityID, h)
|
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
dev.Toggle()
|
|
|
|
})
|
|
|
|
|
2024-09-25 08:06:18 -06:00
|
|
|
card := widget.NewCard("", entityName, container.NewVBox(
|
2024-08-19 19:43:17 -06:00
|
|
|
container.NewHBox(entityButton),
|
|
|
|
))
|
2024-09-25 08:06:18 -06:00
|
|
|
|
|
|
|
card.Refresh()
|
|
|
|
|
|
|
|
return card
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
|
|
|
|
2024-09-22 10:23:57 -06:00
|
|
|
func loadSavedData(a fyne.App, w fyne.Window, input *widget.Entry, file string) {
|
2024-08-23 20:42:03 -06:00
|
|
|
uri, err := storage.Child(a.Storage().RootURI(), file)
|
2024-08-19 19:43:17 -06:00
|
|
|
if err != nil {
|
2024-08-23 20:42:03 -06:00
|
|
|
return
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
|
|
|
|
2024-08-23 20:42:03 -06:00
|
|
|
reader, err := storage.Reader(uri)
|
|
|
|
if err != nil {
|
|
|
|
return
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
2024-08-23 20:42:03 -06:00
|
|
|
defer reader.Close()
|
2024-08-19 19:43:17 -06:00
|
|
|
|
2024-08-23 20:42:03 -06:00
|
|
|
content, err := io.ReadAll(reader)
|
|
|
|
if err != nil {
|
2024-09-22 10:23:57 -06:00
|
|
|
dialog.ShowError(err, w)
|
2024-08-23 20:42:03 -06:00
|
|
|
return
|
|
|
|
}
|
2024-08-19 19:43:17 -06:00
|
|
|
|
2024-08-23 20:42:03 -06:00
|
|
|
input.SetText(string(content))
|
|
|
|
}
|
|
|
|
|
|
|
|
func saveData(a fyne.App, w fyne.Window, input *widget.Entry, file string) {
|
|
|
|
uri, err := storage.Child(a.Storage().RootURI(), file)
|
2024-08-19 19:43:17 -06:00
|
|
|
if err != nil {
|
2024-08-23 20:42:03 -06:00
|
|
|
dialog.ShowError(err, w)
|
|
|
|
return
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
2024-08-23 20:42:03 -06:00
|
|
|
writer, err := storage.Writer(uri)
|
2024-08-19 19:43:17 -06:00
|
|
|
if err != nil {
|
2024-08-23 20:42:03 -06:00
|
|
|
dialog.ShowError(err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer writer.Close()
|
|
|
|
|
|
|
|
_, err = writer.Write([]byte(input.Text))
|
|
|
|
if err != nil {
|
|
|
|
dialog.ShowError(err, w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
dialog.ShowInformation("Success", "", w)
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
a := app.New()
|
|
|
|
w := a.NewWindow("fass")
|
|
|
|
if w == nil {
|
|
|
|
log.Fatalln("unable to create window")
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
var lightCards []fyne.CanvasObject
|
|
|
|
var switchCards []fyne.CanvasObject
|
2024-08-23 20:42:03 -06:00
|
|
|
|
|
|
|
haFile, _ := storage.Child(a.Storage().RootURI(), "haurl")
|
|
|
|
haExists, _ := storage.Exists(haFile)
|
|
|
|
tokenFile, _ := storage.Child(a.Storage().RootURI(), "hatoken")
|
|
|
|
tkExists, _ := storage.Exists(tokenFile)
|
2024-09-22 10:23:57 -06:00
|
|
|
certFile, _ := storage.Child(a.Storage().RootURI(), "haCAcert")
|
|
|
|
certExists, _ := storage.Exists(certFile)
|
2024-08-23 20:42:03 -06:00
|
|
|
|
|
|
|
urlEntry := widget.NewEntry()
|
|
|
|
passEntry := widget.NewPasswordEntry()
|
2024-09-22 10:23:57 -06:00
|
|
|
certEntry := widget.NewMultiLineEntry()
|
2024-08-23 20:42:03 -06:00
|
|
|
|
2024-09-22 10:23:57 -06:00
|
|
|
loadSavedData(a, w, urlEntry, "haurl")
|
|
|
|
loadSavedData(a, w, passEntry, "hatoken")
|
|
|
|
loadSavedData(a, w, certEntry, "haCAcert")
|
2024-08-23 20:42:03 -06:00
|
|
|
|
|
|
|
h := hass.NewAccess(urlEntry.Text, "")
|
|
|
|
if haExists && tkExists {
|
2024-09-25 08:06:18 -06:00
|
|
|
if certExists && certEntry.Text != "" {
|
2024-09-22 10:23:57 -06:00
|
|
|
rootCAs, _ := x509.SystemCertPool()
|
|
|
|
if rootCAs == nil {
|
|
|
|
rootCAs = x509.NewCertPool()
|
|
|
|
}
|
|
|
|
|
|
|
|
if ok := rootCAs.AppendCertsFromPEM([]byte(certEntry.Text)); !ok {
|
|
|
|
dialog.ShowError(fmt.Errorf("No certs appended, using system certs only"), w)
|
|
|
|
}
|
|
|
|
|
|
|
|
client := &http.Client{
|
|
|
|
Timeout: time.Second * 10,
|
|
|
|
Transport: &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
RootCAs: rootCAs,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
h.SetClient(client)
|
|
|
|
}
|
2024-08-23 20:42:03 -06:00
|
|
|
h.SetBearerToken(passEntry.Text)
|
|
|
|
err := h.CheckAPI()
|
|
|
|
if err != nil {
|
|
|
|
dialog.ShowError(err, w)
|
|
|
|
} else {
|
|
|
|
loadData(h, &lightCards, &switchCards)
|
|
|
|
}
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
|
|
|
|
2024-08-23 20:42:03 -06:00
|
|
|
settingsForm := &widget.Form{
|
|
|
|
Items: []*widget.FormItem{
|
|
|
|
{Text: "Home Assistant URL:", Widget: urlEntry},
|
|
|
|
{Text: "Access Token:", Widget: passEntry},
|
2024-09-22 10:23:57 -06:00
|
|
|
{Text: "CA Certificate:", Widget: certEntry},
|
2024-08-23 20:42:03 -06:00
|
|
|
{Text: "", Widget: widget.NewButton("Save", func() {
|
|
|
|
saveData(a, w, urlEntry, "haurl")
|
|
|
|
saveData(a, w, passEntry, "hatoken")
|
2024-09-22 10:23:57 -06:00
|
|
|
saveData(a, w, certEntry, "haCAcert")
|
2024-08-23 20:42:03 -06:00
|
|
|
})},
|
|
|
|
},
|
2024-08-19 19:43:17 -06:00
|
|
|
}
|
|
|
|
|
2024-08-23 20:42:03 -06:00
|
|
|
ctrlQ := &desktop.CustomShortcut{KeyName: fyne.KeyQ, Modifier: fyne.KeyModifierControl}
|
|
|
|
ctrlW := &desktop.CustomShortcut{KeyName: fyne.KeyW, Modifier: fyne.KeyModifierControl}
|
|
|
|
w.Canvas().AddShortcut(ctrlQ, func(shortcut fyne.Shortcut) {
|
|
|
|
a.Quit()
|
|
|
|
})
|
|
|
|
w.Canvas().AddShortcut(ctrlW, func(shortcut fyne.Shortcut) {
|
|
|
|
w.Hide()
|
|
|
|
})
|
|
|
|
|
2024-09-25 08:06:18 -06:00
|
|
|
cols := 5
|
2024-08-19 19:43:17 -06:00
|
|
|
tabs := container.NewAppTabs(
|
|
|
|
container.NewTabItemWithIcon("Lights",
|
|
|
|
theme.VisibilityIcon(),
|
2024-09-25 08:06:18 -06:00
|
|
|
container.NewAdaptiveGrid(cols, lightCards...),
|
|
|
|
),
|
2024-08-19 19:43:17 -06:00
|
|
|
container.NewTabItemWithIcon("Switches",
|
|
|
|
theme.RadioButtonIcon(),
|
2024-09-25 08:06:18 -06:00
|
|
|
container.NewAdaptiveGrid(cols, switchCards...),
|
2024-08-19 19:43:17 -06:00
|
|
|
),
|
|
|
|
)
|
|
|
|
tabs.SetTabLocation(container.TabLocationLeading)
|
2024-08-23 20:42:03 -06:00
|
|
|
|
|
|
|
w.SetContent(
|
|
|
|
container.NewAppTabs(
|
|
|
|
container.NewTabItem("Toggles", tabs),
|
|
|
|
container.NewTabItem("Settings", container.NewStack(
|
|
|
|
settingsForm,
|
|
|
|
)),
|
|
|
|
),
|
|
|
|
)
|
2024-08-19 19:43:17 -06:00
|
|
|
w.SetCloseIntercept(func() {
|
|
|
|
w.Hide()
|
|
|
|
})
|
|
|
|
w.ShowAndRun()
|
|
|
|
}
|