-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3ae2e71
Showing
21 changed files
with
1,272 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
linters: | ||
enable-all: true | ||
disable: | ||
- bodyclose | ||
- depguard | ||
- exhaustruct | ||
- exportloopref | ||
- nonamedreturns | ||
- wrapcheck | ||
# Temporarily disabling while I figure out why severity settings are not working | ||
- cyclop | ||
- funlen | ||
- gocognit | ||
- godox | ||
|
||
linters-settings: | ||
exhaustive: | ||
default-signifies-exhaustive: true | ||
tagliatelle: | ||
case: | ||
rules: | ||
json: snake | ||
|
||
severity: | ||
default-severity: info | ||
rules: | ||
- linters: | ||
- funlen | ||
- gocognit | ||
- godox | ||
- nestif | ||
severity: info | ||
|
||
issues: | ||
exclude: | ||
- "block should not end with a whitespace" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.PHONY: lint | ||
lint: | ||
which golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin v1.62.2 | ||
golangci-lint run | ||
|
||
.PHONY: test | ||
test: | ||
go test -v ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# Home Automation Logic (HAL) Framework | ||
|
||
HAL is a framework for programming home automation logic in Golang using Home | ||
Assistant. | ||
|
||
The hypothesis is that programming (and debugging) automation logic is easier | ||
to do in a programming language rather than YAML or tapping around in a UI. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package hal | ||
|
||
type Automation interface { | ||
Name() string | ||
Entities() Entities | ||
Action() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package halautomations | ||
|
||
import ( | ||
"log/slog" | ||
"time" | ||
|
||
"github.com/dansimau/hal" | ||
) | ||
|
||
// SensorsTriggerLights is an automation that combines one or more sensors ( | ||
// motion or presence sensors) and a set of lights. Lights are turned on when | ||
// any of the sensors are triggered and turned off after a given duration. | ||
type SensorsTriggerLights struct { | ||
name string | ||
|
||
sensors []hal.EntityLike | ||
lights []*hal.Light | ||
turnsOffAfter *time.Duration | ||
|
||
turnOffTimer *time.Timer | ||
} | ||
|
||
func NewSensorsTriggersLights() *SensorsTriggerLights { | ||
return &SensorsTriggerLights{} | ||
} | ||
|
||
func (a *SensorsTriggerLights) WithName(name string) *SensorsTriggerLights { | ||
a.name = name | ||
|
||
return a | ||
} | ||
|
||
func (a *SensorsTriggerLights) WithSensors(sensors ...hal.EntityLike) *SensorsTriggerLights { | ||
a.sensors = sensors | ||
|
||
return a | ||
} | ||
|
||
func (a *SensorsTriggerLights) WithLights(lights ...*hal.Light) *SensorsTriggerLights { | ||
a.lights = lights | ||
|
||
return a | ||
} | ||
|
||
func (a *SensorsTriggerLights) TurnsOffAfter(turnsOffAfter time.Duration) *SensorsTriggerLights { | ||
a.turnsOffAfter = &turnsOffAfter | ||
|
||
return a | ||
} | ||
|
||
// triggered returns true if any of the sensors have been triggered. | ||
func (a *SensorsTriggerLights) triggered() bool { | ||
for _, sensor := range a.sensors { | ||
if sensor.GetState().State == "on" { | ||
return true | ||
} | ||
} | ||
|
||
return false | ||
} | ||
|
||
func (a *SensorsTriggerLights) startTurnOffTimer() { | ||
if a.turnsOffAfter == nil { | ||
return | ||
} | ||
|
||
if a.turnOffTimer == nil { | ||
a.turnOffTimer = time.AfterFunc(*a.turnsOffAfter, a.turnOffLights) | ||
} else { | ||
a.turnOffTimer.Reset(*a.turnsOffAfter) | ||
} | ||
} | ||
|
||
func (a *SensorsTriggerLights) stopTurnOffTimer() { | ||
if a.turnOffTimer != nil { | ||
a.turnOffTimer.Stop() | ||
} | ||
} | ||
|
||
func (a *SensorsTriggerLights) turnOnLights() { | ||
for _, light := range a.lights { | ||
if err := light.TurnOn(); err != nil { | ||
slog.Error("Error turning on light", "error", err) | ||
} | ||
} | ||
} | ||
|
||
func (a *SensorsTriggerLights) turnOffLights() { | ||
for _, light := range a.lights { | ||
if err := light.TurnOff(); err != nil { | ||
slog.Error("Error turning off light", "error", err) | ||
} | ||
} | ||
} | ||
|
||
func (a *SensorsTriggerLights) Action() { | ||
if a.triggered() { | ||
a.stopTurnOffTimer() | ||
a.turnOnLights() | ||
} else { | ||
a.startTurnOffTimer() | ||
} | ||
} | ||
|
||
func (a *SensorsTriggerLights) Entities() hal.Entities { | ||
return hal.Entities(a.sensors) | ||
} | ||
|
||
func (a *SensorsTriggerLights) Name() string { | ||
return a.name | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package hal | ||
|
||
import ( | ||
"log/slog" | ||
"sync" | ||
"time" | ||
|
||
"github.com/dansimau/hal/hassws" | ||
"github.com/dansimau/hal/perf" | ||
"github.com/davecgh/go-spew/spew" | ||
) | ||
|
||
// Connection is a new instance of the HAL framework. It connects to Home Assistant, | ||
// listens for state updates and invokes automations when state changes are detected. | ||
// TODO: Rename "Connection" to something more descriptive. | ||
type Connection struct { | ||
homeAssistant *hassws.Client | ||
|
||
automations map[string][]Automation | ||
entities map[string]EntityLike | ||
|
||
// Lock to serialize state updates and ensure automations fire in order. | ||
mutex sync.RWMutex | ||
} | ||
|
||
// ConnectionBinder is an interface that can be implemented by entities to bind | ||
// them to a connection. | ||
type ConnectionBinder interface { | ||
BindConnection(connection *Connection) | ||
} | ||
|
||
func NewConnection(api *hassws.Client) *Connection { | ||
return &Connection{ | ||
homeAssistant: api, | ||
|
||
automations: make(map[string][]Automation), | ||
entities: make(map[string]EntityLike), | ||
} | ||
} | ||
|
||
// HomeAssistant returns the underlying Home Assistant websocket client. | ||
func (h *Connection) HomeAssistant() *hassws.Client { | ||
return h.homeAssistant | ||
} | ||
|
||
// FindEntities recursively finds and registers all entities in a struct, map, or slice. | ||
func (h *Connection) FindEntities(v any) { | ||
h.RegisterEntities(findEntities(v)...) | ||
} | ||
|
||
// RegisterAutomations registers automations and binds them to the relevant entities. | ||
func (h *Connection) RegisterAutomations(automations ...Automation) { | ||
for _, automation := range automations { | ||
for _, entity := range automation.Entities() { | ||
h.automations[entity.GetID()] = append(h.automations[entity.GetID()], automation) | ||
} | ||
} | ||
} | ||
|
||
// RegisterEntities registers entities and binds them to the connection. | ||
func (h *Connection) RegisterEntities(entities ...EntityLike) { | ||
for _, entity := range entities { | ||
slog.Info("Registering entity", "EntityID", entity.GetID()) | ||
entity.BindConnection(h) | ||
h.entities[entity.GetID()] = entity | ||
} | ||
} | ||
|
||
// Start connects to the Home Assistant websocket and starts listening for events. | ||
func (h *Connection) Start() error { | ||
if err := h.HomeAssistant().Connect(); err != nil { | ||
return err | ||
} | ||
|
||
return h.HomeAssistant().SubscribeEvents(string(hassws.MessageTypeStateChanged), h.StateChangeEvent) | ||
} | ||
|
||
// Process incoming state change events. Dispatch state change to the relevant | ||
// entity and fire any automations listening for state changes to this entity. | ||
func (h *Connection) StateChangeEvent(event hassws.EventMessage) { | ||
defer perf.Timer(func(timeTaken time.Duration) { | ||
slog.Debug("Tick processing time", "duration", timeTaken) | ||
})() | ||
|
||
h.mutex.Lock() | ||
defer h.mutex.Unlock() | ||
|
||
entity, ok := h.entities[event.Event.EventData.EntityID] | ||
if !ok { | ||
slog.Debug("Entity not registered", "EntityID", event.Event.EventData.EntityID) | ||
|
||
return | ||
} | ||
|
||
slog.Debug("State changed for", | ||
"EntityID", event.Event.EventData.EntityID, | ||
"NewState", spew.Sdump(event.Event.EventData.NewState), | ||
) | ||
|
||
if event.Event.EventData.NewState != nil { | ||
entity.SetState(*event.Event.EventData.NewState) | ||
} | ||
|
||
// Dispatch automations | ||
for _, automation := range h.automations[event.Event.EventData.EntityID] { | ||
automation.Action() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package hal_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/dansimau/hal" | ||
"github.com/dansimau/hal/homeassistant" | ||
"github.com/dansimau/hal/testutil" | ||
) | ||
|
||
func TestConnection(t *testing.T) { | ||
t.Parallel() | ||
|
||
conn, server, cleanup := testutil.NewClientServer(t) | ||
defer cleanup() | ||
|
||
// Create test entity and register it | ||
entity := hal.NewEntity("test.entity") | ||
conn.RegisterEntities(entity) | ||
|
||
// Send state change event | ||
server.SendStateChangeEvent(homeassistant.Event{ | ||
EventData: homeassistant.EventData{ | ||
EntityID: "test.entity", | ||
NewState: &homeassistant.State{State: "on"}, | ||
}, | ||
}) | ||
|
||
// Verify entity state was updated | ||
testutil.WaitFor(t, func() bool { | ||
return entity.State.State == "on" | ||
}) | ||
} |
Oops, something went wrong.