diff --git a/.scripts/Install.ps1 b/.scripts/Install.ps1 new file mode 100644 index 0000000..6cd694d --- /dev/null +++ b/.scripts/Install.ps1 @@ -0,0 +1,31 @@ +# A build & install script for the project's Windows version. +$ErrorActionPreference = "Stop" +$executableName = "door_tray" + +if (-Not (Test-Path -Path "./go.mod")) { + Write-Error "Please run this script from the project's root directory (go.mod not found)." + exit 1 +} + +# Build +go build -o "bin/$executableName-temp.exe" -ldflags "-s -w" "./cmd/windows/" +if ($LASTEXITCODE -ne 0) { + Write-Error "Build failed with exit code $LASTEXITCODE." + exit 1 +} + +# Compress +upx "bin/$executableName-temp.exe" -o "bin/$executableName.exe" -5 -f +if ($LASTEXITCODE -ne 0) { + Write-Error "Compression failed with exit code $LASTEXITCODE." + exit 1 +} +Remove-Item "bin/$executableName-temp.exe" + +# Setup service +$serviceName = "DoorTray" + +# TODO: Stop old service +# TODO: Install new binary +# TODO: Start & verify service +# TODO: Cleanup, print latest logs \ No newline at end of file diff --git a/README.md b/README.md index 5c2974e..4118c01 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -# hass-tray +# door-tray A simple Go application to display basic state details (via a tray icon) from a Home Assistant instance. +- Easy install with a single binary + - Windows (Service), Linux (Systemd) - Ultra simple configuration - YAML configuration file - Environment variables diff --git a/cmd/windows/main.go b/cmd/windows/main.go new file mode 100644 index 0000000..50d46e0 --- /dev/null +++ b/cmd/windows/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "log" + "log/slog" + "time" + + "internal" + + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/debug" +) + +type WrapperService struct { + service internal.Service +} + +func (wrapper *WrapperService) Execute(args []string, requestChannel <-chan svc.ChangeRequest, status chan<- svc.Status) (bool, uint32) { + const acceptedCommands = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue + tick := time.Tick(5 * time.Second) + + status <- svc.Status{State: svc.StartPending} + status <- svc.Status{State: svc.Running, Accepts: acceptedCommands} + +loop: + for { + select { + case <-tick: + slog.Debug("Tick Handled...!") + case changeRequest := <-requestChannel: + switch changeRequest.Cmd { + case svc.Interrogate: + slog.Debug("Interrogate Requested", "changeRequest", changeRequest) + status <- changeRequest.CurrentStatus + case svc.Stop, svc.Shutdown: + slog.Warn("Shutdown Requested", "changeRequest", changeRequest) + break loop + case svc.Pause: + wrapper.service.Pause() + slog.Warn("Pause Requested", "changeRequest", changeRequest) + status <- svc.Status{State: svc.Paused, Accepts: acceptedCommands} + case svc.Continue: + slog.Info("Continue Requested", "changeRequest", changeRequest) + status <- svc.Status{State: svc.Running, Accepts: acceptedCommands} + default: + slog.Warn("Unexpected Change Request", "changeRequest", changeRequest) + } + } + } + + slog.Info("Service Stopping") + status <- svc.Status{State: svc.StopPending} + return false, 1 +} + +func runService(name string, isDebug bool) { + service := WrapperService{ + service: internal.NewApp(), + } + + if isDebug { + err := debug.Run(name, &service) + if err != nil { + log.Fatalln("Error running service in debug mode.") + } + } else { + err := svc.Run(name, &service) + if err != nil { + log.Fatalln("Error running service in Service Control mode.") + } + } +} + +func main() { + + runService("DoorTray", true) +} diff --git a/configs/hass_tray.service b/configs/hass_tray.service new file mode 100644 index 0000000..af33905 --- /dev/null +++ b/configs/hass_tray.service @@ -0,0 +1,27 @@ +[Unit] +Description=A description +ConditionPathExists=/home/ubuntu/work/src/door_tray/door_tray +After=network.target + +[Service] +Type=simple +User=door_tray +Group=door_tray +LimitNOFILE=1024 + +Restart=on-failure +RestartSec=10 +startLimitIntervalSec=60 + +WorkingDirectory=/home/ubuntu/work/src/door_tray +ExecStart=/home/ubuntu/work/src/door_tray/door_tray --name=foo + +# make sure log directory exists and owned by syslog +PermissionsStartOnly=true +ExecStartPre=/bin/mkdir -p /var/log/door_tray +ExecStartPre=/bin/chown syslog:adm /var/log/door_tray +ExecStartPre=/bin/chmod 755 /var/log/door_tray +SyslogIdentifier=door_tray + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/go.mod b/go.mod index dee36e7..d1b8428 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,21 @@ -module xevion.dev/hass-tray +module xevion.dev/door-tray go 1.23.1 +require ( + golang.org/x/sys v0.1.0 + internal v1.0.0 +) + +replace internal => ./internal/ + +require ( + github.com/getlantern/systray v1.2.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + golang.org/x/text v0.19.0 // indirect + saml.dev/gome-assistant v0.2.3 // indirect +) + require ( github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect @@ -9,17 +23,13 @@ require ( github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect - github.com/getlantern/systray v1.2.2 // indirect github.com/go-stack/stack v1.8.0 // indirect github.com/gobuffalo/envy v1.10.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect github.com/gobuffalo/packr v1.30.1 // indirect github.com/golang-module/carbon v1.7.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/nathan-osman/go-sunrise v1.1.0 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - golang.org/x/sys v0.1.0 // indirect - saml.dev/gome-assistant v0.2.3 // indirect ) diff --git a/go.sum b/go.sum index 19c9286..8df370d 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8Nz github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= @@ -40,7 +41,6 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= @@ -61,6 +61,7 @@ github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgF github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -83,6 +84,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -100,6 +103,8 @@ golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -107,6 +112,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= saml.dev/gome-assistant v0.2.3 h1:WuT0yMuUjyM78eHYjry/mbhMypSZ7GKmc4vr+Y4YOQ4= saml.dev/gome-assistant v0.2.3/go.mod h1:NAj56yKBq4PXmHIdrn9oeTmM5SHaNYoLfSWpzTpKjXY= diff --git a/go.work b/go.work new file mode 100644 index 0000000..e2f8485 --- /dev/null +++ b/go.work @@ -0,0 +1,3 @@ +go 1.23.1 + +use ./internal diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..92c146f --- /dev/null +++ b/go.work.sum @@ -0,0 +1,5 @@ +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= diff --git a/internal/app.go b/internal/app.go new file mode 100644 index 0000000..7702ae7 --- /dev/null +++ b/internal/app.go @@ -0,0 +1,190 @@ +package internal + +import ( + "embed" + "fmt" + "log/slog" + "os" + "os/signal" + "runtime" + "time" + + "github.com/getlantern/systray" + dotenv "github.com/joho/godotenv" + "golang.org/x/text/cases" + "golang.org/x/text/language" + ga "saml.dev/gome-assistant" +) + +var () + +type TrayApp struct { + doorIdentifier string + log *slog.Logger + stateChannel chan string + app *ga.App + service *ga.Service +} + +// Status will return the operational status of the service +func (ta *TrayApp) Status() Status { + return StatusUnknown +} + +func (ta *TrayApp) State() string { + // TODO: Implement this method + return "" +} + +func (ta *TrayApp) Connected() bool { + // TODO: Implement this method + return false +} + +func (ta *TrayApp) Reload() error { + // TODO: Implement this method + return nil +} + +func (ta *TrayApp) Pause() error { + // TODO: Implement this method + return nil +} + +func (ta *TrayApp) Resume() error { + // TODO: Implement this method + return nil +} + +func NewApp() *TrayApp { + // Connect to Home Assistant + app, err := ga.NewApp(ga.NewAppRequest{ + IpAddress: "home.imfucked.lol", // Replace with your Home Assistant IP Address + HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), + HomeZoneEntityId: "zone.home", + Port: "443", + Secure: true, + }) + if err != nil { + slog.Error("Error connecting to Home Assistant", "error", err) + os.Exit(1) + } + + service := app.GetService() + + return &TrayApp{ + app: app, + service: service, + stateChannel: make(chan string), + doorIdentifier: "binary_sensor.bedroom_door_opening", + } +} + +var ( + //go:embed "resources/*.ico" + icons embed.FS +) + +func (ta *TrayApp) HandleState(newState string) { + switch newState { + case "on": + ta.stateChannel <- "open" + case "off": + ta.stateChannel <- "closed" + default: + slog.Error("unknown state encountered", "newState", newState) + ta.stateChannel <- "unknown" + } +} + +func (ta *TrayApp) setupHomeAssistant() { + var err error + + // Get the initial state + state, err := ta.app.GetState().Get(ta.doorIdentifier) + if err != nil { + slog.Error("Unable to get initial state", "error", err) + } else { + slog.Debug("Initial State Received") + ta.HandleState(state.State) + } + + ta.app.RegisterEntityListeners(ga. + NewEntityListener(). + EntityIds(ta.doorIdentifier). + Call(func(service *ga.Service, state ga.State, sensor ga.EntityData) { + slog.Debug("Event Received", "identifier", ta.doorIdentifier, "sensor", sensor) + ta.HandleState(sensor.ToState) + }). + Build()) + + ta.app.Start() + + slog.Warn("Home Assistant thread died") + ta.stateChannel <- "unknown" +} + +func (ta *TrayApp) Start() { + dotenv.Load() + + slog.SetDefault(slog.New(slog.NewJSONHandler( + os.Stdout, + &slog.HandlerOptions{ + Level: slog.LevelDebug, + }, + ))) + // binfo, err := buildinfo. + slog.Info("Startup", "runtime", runtime.Version(), "os", runtime.GOOS, "arch", runtime.GOARCH, "pid", os.Getpid()) + + go ta.setupHomeAssistant() + systray.Run(ta.onReady, func() {}) +} + +func (ta *TrayApp) onReady() { + systray.SetTitle("door-tray") + systray.SetTooltip("Setting up...") + menuQuit := systray.AddMenuItem("Quit", "Stops the application") + menuOpenLogs := systray.AddMenuItem("Open Logs", "Opens the logs in the default editor") + menuOpenLogs.Disable() + + // Load icons + systray.SetIcon(getIcon("unknown")) + + // Handle Ctrl+C interrupt + interruptChannel := make(chan os.Signal, 1) + signal.Notify(interruptChannel, os.Interrupt) + signal.Notify(interruptChannel, os.Kill) + +loop: + for { + select { + case signal := <-interruptChannel: + slog.Info("Received interrupt signal, quitting", "signal", signal) + break loop + case <-menuQuit.ClickedCh: + slog.Info("Quit clicked") + break loop + case <-menuOpenLogs.ClickedCh: + slog.Info("Open Logs clicked") + case newState := <-ta.stateChannel: + timeString := time.Now().Format("3:04 PM") + if newState != "unknown" { + systray.SetTooltip(fmt.Sprintf("%s as of %s", cases.Title(language.AmericanEnglish, cases.NoLower).String(newState), timeString)) + switch newState { + case "open": + systray.SetIcon(getIcon("open_fault")) + case "closed": + systray.SetIcon(getIcon("closed")) + } + } else { + slog.Warn("Unknown state", "state", newState) + systray.SetTooltip(fmt.Sprintf("Unknown as of %s", timeString)) + systray.SetIcon(getIcon("unknown")) + } + } + } + + slog.Info("Cleaning up") + systray.Quit() + ta.app.Cleanup() +} diff --git a/internal/config.go b/internal/config.go new file mode 100644 index 0000000..a207a21 --- /dev/null +++ b/internal/config.go @@ -0,0 +1,16 @@ +package internal + +import ( + "fmt" + "log/slog" + "os" +) + +func getIcon(icon string) []byte { + iconBytes, err := icons.ReadFile(fmt.Sprintf("resources/%s.ico", icon)) + if err != nil { + slog.Error("Unable to load icon", "error", err) + os.Exit(1) + } + return iconBytes +} diff --git a/internal/go.mod b/internal/go.mod new file mode 100644 index 0000000..4630e8c --- /dev/null +++ b/internal/go.mod @@ -0,0 +1,26 @@ +module internal + +go 1.22.0 + +require ( + github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect + github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect + github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect + github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect + github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect + github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect + github.com/getlantern/systray v1.2.2 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/gobuffalo/envy v1.10.2 // indirect + github.com/gobuffalo/packd v1.0.2 // indirect + github.com/gobuffalo/packr v1.30.1 // indirect + github.com/golang-module/carbon v1.7.1 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/nathan-osman/go-sunrise v1.1.0 // indirect + github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/text v0.19.0 // indirect + saml.dev/gome-assistant v0.2.3 // indirect +) diff --git a/internal/go.sum b/internal/go.sum new file mode 100644 index 0000000..bb92348 --- /dev/null +++ b/internal/go.sum @@ -0,0 +1,113 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4= +github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So= +github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A= +github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk= +github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0= +github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc= +github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA= +github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA= +github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE= +github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= +github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw= +github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= +github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= +github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= +github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/golang-module/carbon v1.7.1 h1:EDPV0YjxeS2kE2cRedfGgDikU6l5D79HB/teHuZDLu8= +github.com/golang-module/carbon v1.7.1/go.mod h1:M/TDTYPp3qWtW68u49dLDJOyGmls6L6BXdo/pyvkMaU= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= +github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8= +github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= +github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +saml.dev/gome-assistant v0.2.3 h1:WuT0yMuUjyM78eHYjry/mbhMypSZ7GKmc4vr+Y4YOQ4= +saml.dev/gome-assistant v0.2.3/go.mod h1:NAj56yKBq4PXmHIdrn9oeTmM5SHaNYoLfSWpzTpKjXY= diff --git a/resources/closed.ico b/internal/resources/closed.ico similarity index 100% rename from resources/closed.ico rename to internal/resources/closed.ico diff --git a/internal/resources/error.ico b/internal/resources/error.ico new file mode 100644 index 0000000..5ada517 Binary files /dev/null and b/internal/resources/error.ico differ diff --git a/resources/open.ico b/internal/resources/open.ico similarity index 100% rename from resources/open.ico rename to internal/resources/open.ico diff --git a/internal/resources/open_fault.ico b/internal/resources/open_fault.ico new file mode 100644 index 0000000..d7dec93 Binary files /dev/null and b/internal/resources/open_fault.ico differ diff --git a/resources/unknown.ico b/internal/resources/unknown.ico similarity index 100% rename from resources/unknown.ico rename to internal/resources/unknown.ico diff --git a/internal/types.go b/internal/types.go new file mode 100644 index 0000000..0191cf6 --- /dev/null +++ b/internal/types.go @@ -0,0 +1,19 @@ +package internal + +type Status int + +const ( + StatusUnknown Status = iota + StatusRunning + StatusStopped + StatusError +) + +type Service interface { + Status() Status + State() string + Connected() bool + Reload() error + Pause() error + Resume() error +} diff --git a/internal/utility.go b/internal/utility.go new file mode 100644 index 0000000..87a7008 --- /dev/null +++ b/internal/utility.go @@ -0,0 +1,14 @@ +package internal + +import "runtime/debug" + +var Commit = func() string { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + return setting.Value + } + } + } + return "" +}() diff --git a/main.go b/main.go deleted file mode 100644 index db1b3e1..0000000 --- a/main.go +++ /dev/null @@ -1,150 +0,0 @@ -package main - -import ( - "embed" - "log/slog" - "os" - "os/signal" - - "github.com/getlantern/systray" - dotenv "github.com/joho/godotenv" - ga "saml.dev/gome-assistant" -) - -var ( - doorIdentifier = "binary_sensor.bedroom_door_opening" - service *ga.Service - log *slog.Logger - stateChannel chan string - app *ga.App -) - -var ( - //go:embed "resources/*.ico" - icons embed.FS -) - -func HandleState(newState string) { - switch newState { - case "on": - stateChannel <- "open" - case "off": - stateChannel <- "closed" - default: - slog.Error("unknown state encountered", "newState", newState) - stateChannel <- "unknown" - } -} - -func setupHomeAssistant() { - var err error - - // Connect to Home Assistant - app, err = ga.NewApp(ga.NewAppRequest{ - IpAddress: "home.imfucked.lol", // Replace with your Home Assistant IP Address - HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), - HomeZoneEntityId: "zone.home", - Port: "443", - Secure: true, - }) - if err != nil { - log.Error("Error connecting to Home Assistant", "error", err) - os.Exit(1) - } - - service = app.GetService() - - // Get the initial state - state, err := app.GetState().Get(doorIdentifier) - if err != nil { - slog.Error("Unable to get initial state", "error", err) - } else { - slog.Debug("Initial State Received") - HandleState(state.State) - } - - app.RegisterEntityListeners(ga. - NewEntityListener(). - EntityIds(doorIdentifier). - Call(func(service *ga.Service, state ga.State, sensor ga.EntityData) { - slog.Debug("Event Received", "identifier", doorIdentifier, "sensor", sensor) - HandleState(sensor.ToState) - }). - Build()) - - app.Start() -} - -func main() { - dotenv.Load() - stateChannel = make(chan string) - - slog.SetDefault(slog.New(slog.NewJSONHandler( - os.Stdout, - &slog.HandlerOptions{ - Level: slog.LevelDebug, - }, - ))) - slog.Info("Starting hass-tray") - - go setupHomeAssistant() - systray.Run(onReady, func() {}) -} - -func onReady() { - systray.SetTitle("hass-tray") - systray.SetTooltip("Refreshed") - menuQuit := systray.AddMenuItem("Quit", "Stops the application") - menuOpenLogs := systray.AddMenuItem("Open Logs", "Opens the logs in the default editor") - menuOpenLogs.Disable() - - // Load icons - openIcon, err := icons.ReadFile("resources/open.ico") - if err != nil { - slog.Error("Unable to load icon", "error", err) - os.Exit(1) - } - closedIcon, err := icons.ReadFile("resources/closed.ico") - if err != nil { - slog.Error("Unable to load icon", "error", err) - os.Exit(1) - } - unknownIcon, err := icons.ReadFile("resources/unknown.ico") - if err != nil { - slog.Error("Unable to load icon", "error", err) - os.Exit(1) - } else { - slog.Debug("Icons loaded") - } - systray.SetIcon(unknownIcon) - - // Handle Ctrl+C interrupt - interruptChannel := make(chan os.Signal, 1) - signal.Notify(interruptChannel, os.Interrupt) - signal.Notify(interruptChannel, os.Kill) - -loop: - for { - select { - case signal := <-interruptChannel: - slog.Info("Received interrupt signal, quitting", "signal", signal) - break loop - case <-menuQuit.ClickedCh: - slog.Info("Quit clicked") - break loop - case newState := <-stateChannel: - if newState == "open" { - systray.SetIcon(openIcon) - } else if newState == "closed" { - systray.SetIcon(closedIcon) - } else { - slog.Warn("Unknown state", "state", newState) - systray.SetIcon(unknownIcon) - } - } - } - - slog.Info("Cleaning up") - systray.Quit() - app.Cleanup() -} diff --git a/resources/result.ico b/resources/result.ico deleted file mode 100644 index 173ea4c..0000000 Binary files a/resources/result.ico and /dev/null differ