Skip to content

Commit

Permalink
Various fixes and improvements
Browse files Browse the repository at this point in the history
* Add generic automation builder
* Add BinarySensor, LightSensor, LightGroup
* Add LightInterface and move towards that
* Add Timer helper
* Add more logging
* Fix decoding JSON attributes
  • Loading branch information
dansimau committed Dec 11, 2024
1 parent 4f78b0d commit 4e37dea
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 33 deletions.
44 changes: 44 additions & 0 deletions automations.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,47 @@ type Automation interface {
// Action is called when the automation is triggered.
Action()
}

type AutomationConfig struct {
action func()
entities Entities
name string
}

func NewAutomation() *AutomationConfig {
return &AutomationConfig{}
}

func (c *AutomationConfig) Entities() Entities {
return c.entities
}

func (c *AutomationConfig) Action() {
c.action()
}

func (c *AutomationConfig) Name() string {
return c.name
}

func (c *AutomationConfig) WithAction(action func()) *AutomationConfig {
c.action = action

if c.name == "" {
c.name = getShortFunctionName(action)
}

return c
}

func (c *AutomationConfig) WithEntities(entities ...EntityInterface) *AutomationConfig {
c.entities = entities

return c
}

func (c *AutomationConfig) WithName(name string) *AutomationConfig {
c.name = name

return c
}
31 changes: 31 additions & 0 deletions automations/print_debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package halautomations

import (
"log"

"github.com/dansimau/hal"
)

// PrintDebug prints state changes for the specified entities.
type PrintDebug struct {
name string
entities hal.Entities
}

func NewPrintDebug(name string, entities ...hal.EntityInterface) *PrintDebug {
return &PrintDebug{name: name, entities: entities}
}

func (p *PrintDebug) Name() string {
return p.name
}

func (p *PrintDebug) Entities() hal.Entities {
return p.entities
}

func (p *PrintDebug) Action() {
for _, entity := range p.entities {
log.Printf("[%s] Entity %s state: %+v", p.name, entity.GetID(), entity.GetState())
}
}
18 changes: 13 additions & 5 deletions automations/sensor_lights.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,35 @@ import (
// any of the sensors are triggered and turned off after a given duration.
type SensorsTriggerLights struct {
name string
log *slog.Logger

sensors []hal.EntityLike
lights []*hal.Light
sensors []hal.EntityInterface
lights []hal.LightInterface
turnsOffAfter *time.Duration

turnOffTimer *time.Timer
}

func NewSensorsTriggersLights() *SensorsTriggerLights {
return &SensorsTriggerLights{}
return &SensorsTriggerLights{
log: slog.Default(),
}
}

func (a *SensorsTriggerLights) WithName(name string) *SensorsTriggerLights {
a.name = name
a.log = slog.With("automation", a.name)

return a
}

func (a *SensorsTriggerLights) WithSensors(sensors ...hal.EntityLike) *SensorsTriggerLights {
func (a *SensorsTriggerLights) WithSensors(sensors ...hal.EntityInterface) *SensorsTriggerLights {
a.sensors = sensors

return a
}

func (a *SensorsTriggerLights) WithLights(lights ...*hal.Light) *SensorsTriggerLights {
func (a *SensorsTriggerLights) WithLights(lights ...hal.LightInterface) *SensorsTriggerLights {
a.lights = lights

return a
Expand Down Expand Up @@ -86,6 +90,8 @@ func (a *SensorsTriggerLights) turnOnLights() {
}

func (a *SensorsTriggerLights) turnOffLights() {
a.log.Info("Turning off lights")

for _, light := range a.lights {
if err := light.TurnOff(); err != nil {
slog.Error("Error turning off light", "error", err)
Expand All @@ -95,9 +101,11 @@ func (a *SensorsTriggerLights) turnOffLights() {

func (a *SensorsTriggerLights) Action() {
if a.triggered() {
a.log.Info("Sensor triggered, turning on lights")
a.stopTurnOffTimer()
a.turnOnLights()
} else {
a.log.Info("Sensor cleared, starting turn off countdown")
a.startTurnOffTimer()
}
}
Expand Down
8 changes: 5 additions & 3 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type Connection struct {
config Config

automations map[string][]Automation
entities map[string]EntityLike
entities map[string]EntityInterface

// Lock to serialize state updates and ensure automations fire in order.
mutex sync.RWMutex
Expand All @@ -44,7 +44,7 @@ func NewConnection(cfg Config) *Connection {
homeAssistant: api,

automations: make(map[string][]Automation),
entities: make(map[string]EntityLike),
entities: make(map[string]EntityInterface),

SunTimes: NewSunTimes(cfg.Location),
}
Expand All @@ -63,14 +63,15 @@ func (h *Connection) FindEntities(v any) {
// RegisterAutomations registers automations and binds them to the relevant entities.
func (h *Connection) RegisterAutomations(automations ...Automation) {
for _, automation := range automations {
slog.Info("Registering automation", "Name", automation.Name())
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) {
func (h *Connection) RegisterEntities(entities ...EntityInterface) {
for _, entity := range entities {
slog.Info("Registering entity", "EntityID", entity.GetID())
entity.BindConnection(h)
Expand Down Expand Up @@ -115,6 +116,7 @@ func (h *Connection) StateChangeEvent(event hassws.EventMessage) {

// Dispatch automations
for _, automation := range h.automations[event.Event.EventData.EntityID] {
slog.Info("Running automation", "name", automation.Name())
automation.Action()
}
}
2 changes: 1 addition & 1 deletion connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ func TestConnection(t *testing.T) {

// Verify entity state was updated
testutil.WaitFor(t, func() bool {
return entity.State.State == "on"
return entity.GetState().State == "on"
})
}
23 changes: 11 additions & 12 deletions entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,26 @@ import (
"github.com/dansimau/hal/homeassistant"
)

// EntityLike is the interface that we accept which allows us to create
// EntityInterface is the interface that we accept which allows us to create
// custom components that embed an Entity.
type EntityLike interface {
type EntityInterface interface {
ConnectionBinder

GetID() string
GetState() homeassistant.State
SetState(event homeassistant.State)
}

type Entities []EntityLike
type Entities []EntityInterface

// Entity is a base type for all entities that can be embedded into other types.
type Entity struct {
connection *Connection

homeassistant.State
state homeassistant.State
}

func NewEntity(id string) *Entity {
return &Entity{State: homeassistant.State{EntityID: id}}
return &Entity{state: homeassistant.State{EntityID: id}}
}

// BindConnection binds the entity to the connection. This allows entities to
Expand All @@ -36,20 +35,20 @@ func (e *Entity) BindConnection(connection *Connection) {
}

func (e *Entity) GetID() string {
return e.State.EntityID
return e.state.EntityID
}

func (e *Entity) SetState(state homeassistant.State) {
e.State = state
e.state = state
}

func (e *Entity) GetState() homeassistant.State {
return e.State
return e.state
}

// findEntities recursively finds all entities in a struct, map, or slice.
func findEntities(v any) []EntityLike {
var entities []EntityLike
func findEntities(v any) []EntityInterface {
var entities []EntityInterface

value := reflect.ValueOf(v)
if value.Kind() == reflect.Ptr {
Expand All @@ -73,7 +72,7 @@ func findEntities(v any) []EntityLike {

// Check if field implements EntityLike interface
if field.Kind() == reflect.Ptr && !field.IsNil() {
if entity, ok := field.Interface().(EntityLike); ok {
if entity, ok := field.Interface().(EntityInterface); ok {
entities = append(entities, entity)

continue
Expand Down
18 changes: 18 additions & 0 deletions entity_binary_sensor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package hal

// BinarySensor is any sensor with a state of "on" or "off".
type BinarySensor struct {
*Entity
}

func NewBinarySensor(id string) *BinarySensor {
return &BinarySensor{Entity: NewEntity(id)}
}

func (s *BinarySensor) IsOff() bool {
return s.GetState().State == "off"
}

func (s *BinarySensor) IsOn() bool {
return s.GetState().State == "on"
}
65 changes: 65 additions & 0 deletions entity_light.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package hal

import (
"errors"
"log/slog"

"github.com/dansimau/hal/hassws"
)

type LightInterface interface {
IsOn() bool
TurnOn() error
TurnOff() error
}

type Light struct {
*Entity
}
Expand All @@ -18,9 +27,12 @@ func (l *Light) IsOn() bool {

func (l *Light) TurnOn() error {
if l.connection == nil {
slog.Error("Light not registered", "entity", l.GetID())
return ErrEntityNotRegistered
}

slog.Debug("Turning on light", "entity", l.GetID())

_, err := l.connection.HomeAssistant().CallService(hassws.CallServiceRequest{
Type: hassws.MessageTypeCallService,
Domain: "light",
Expand All @@ -29,15 +41,21 @@ func (l *Light) TurnOn() error {
"entity_id": []string{l.GetID()},
},
})
if err != nil {
slog.Error("Error turning on light", "entity", l.GetID(), "error", err)
}

return err
}

func (l *Light) TurnOff() error {
if l.connection == nil {
slog.Error("Light not registered", "entity", l.GetID())
return ErrEntityNotRegistered
}

slog.Info("Turning off light", "entity", l.GetID())

_, err := l.connection.HomeAssistant().CallService(hassws.CallServiceRequest{
Type: hassws.MessageTypeCallService,
Domain: "light",
Expand All @@ -46,6 +64,53 @@ func (l *Light) TurnOff() error {
"entity_id": []string{l.GetID()},
},
})
if err != nil {
slog.Error("Error turning off light", "entity", l.GetID(), "error", err)
}

return err
}

type LightGroup []LightInterface

func (lg LightGroup) IsOn() bool {
for _, l := range lg {
if !l.IsOn() {
return false
}
}

return true
}

func (lg LightGroup) TurnOn() error {
var errs []error

for _, l := range lg {
if err := l.TurnOn(); err != nil {
errs = append(errs, err)
}
}

if len(errs) > 1 {
return errors.Join(errs...)
}

return nil
}

func (lg LightGroup) TurnOff() error {
var errs []error

for _, l := range lg {
if err := l.TurnOff(); err != nil {
errs = append(errs, err)
}
}

if len(errs) > 1 {
return errors.Join(errs...)
}

return nil
}
Loading

0 comments on commit 4e37dea

Please sign in to comment.