Merge branch 'beelzebub' of git.aiterp.net:Lucifer3/Server into beelzebub

This commit is contained in:
2022-12-31 17:19:26 +01:00
6 changed files with 458 additions and 11 deletions
+246
View File
@@ -0,0 +1,246 @@
package tradfri
import (
"fmt"
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/device"
"git.aiterp.net/lucifer3/server/events"
"git.aiterp.net/lucifer3/server/internal/color"
"git.aiterp.net/lucifer3/server/internal/gentools"
"github.com/eriklupander/tradfri-go/tradfri"
"math"
"math/rand"
"strings"
"sync"
"time"
)
type Bridge struct {
mx sync.Mutex
client *tradfri.Client
id string
newIP string
newCredentials string
internalMap map[string]int
nameMap map[string]string
stateMap map[string]device.State
}
func connect(ip, credentials string) (bridge *Bridge, retErr error) {
bridge = &Bridge{}
defer func() {
retErr = catchPanic()
}()
if !strings.Contains(ip, ":") {
ip = ip + ":5684"
}
parts := strings.Split(credentials, ":")
if len(parts) == 1 {
bridge.client = tradfri.NewTradfriClient(ip, "Client_identity", parts[0])
clientID := fmt.Sprintf("Lucifer4_%d", rand.Intn(10000))
token, err := bridge.client.AuthExchange(clientID)
if err != nil {
return nil, err
}
bridge.newCredentials = clientID + ":" + token.Token
} else {
bridge.client = tradfri.NewTradfriClient(ip, parts[0], parts[1])
bridge.newCredentials = parts[0] + ":" + parts[1]
}
bridge.newIP = ip
bridge.id = fmt.Sprintf("tradfri:%s", ip)
return
}
func (b *Bridge) listen(bus *lucifer3.EventBus) {
b.internalMap = make(map[string]int, 16)
b.nameMap = make(map[string]string, 16)
b.stateMap = make(map[string]device.State, 16)
go func() {
defer b.mx.Unlock()
for {
b.mx.Lock()
devices, err := b.client.ListDevices()
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: b.id,
Error: "Unable to fetch IKEA devices",
})
return
}
for _, ikeaDevice := range devices {
id := fmt.Sprintf("%s:%d", b.id, ikeaDevice.DeviceId)
lc := ikeaDevice.LightControl
if len(lc) == 0 {
continue
}
currState := device.State{
Power: gentools.Ptr(lc[0].Power > 0),
Intensity: gentools.Ptr(float64(lc[0].Dimmer) / 254),
Color: &color.Color{
XY: &color.XY{
X: float64(lc[0].CIE_1931_X) / 65535,
Y: float64(lc[0].CIE_1931_Y) / 65535,
},
},
}
if len(b.nameMap[id]) == 0 {
b.internalMap[id] = ikeaDevice.DeviceId
b.nameMap[id] = ikeaDevice.Name
b.stateMap[id] = currState
bus.RunEvent(events.DeviceReady{ID: id})
bus.RunEvent(events.HardwareMetadata{ID: id})
bus.RunEvent(events.HardwareState{
ID: id,
InternalName: ikeaDevice.Name,
SupportFlags: defaultSF,
ColorFlags: defaultCF,
State: b.stateMap[id],
})
}
b.refreshDevice(id, bus)
}
b.mx.Unlock()
time.Sleep(10 * time.Second)
}
}()
}
func (b *Bridge) writeState(id string, state device.State, bus *lucifer3.EventBus) {
b.stateMap[id] = state
b.refreshDevice(id, bus)
}
func (b *Bridge) refreshDevice(id string, bus *lucifer3.EventBus) {
ikeaDevice, err := b.client.GetDevice(b.internalMap[id])
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: id,
Error: "Unable to fetch IKEA device",
})
return
}
changed := false
if len(ikeaDevice.LightControl) > 0 {
ikeaState := ikeaDevice.LightControl[0]
luciferState := b.stateMap[id]
currPower := ikeaState.Power > 0
currIntensity := float64(ikeaState.Dimmer) / 254
currX := float64(ikeaState.CIE_1931_X) / 65535
currY := float64(ikeaState.CIE_1931_Y) / 65535
if luciferState.Intensity != nil {
newIntensity := *luciferState.Intensity
diffIntensity := math.Abs(newIntensity - currIntensity)
if diffIntensity >= 0.01 {
changed = true
intensityInt := int(math.Round(newIntensity * 254))
_, err := b.client.PutDeviceDimming(ikeaDevice.DeviceId, intensityInt)
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: id,
Error: "Failed to update intensity state",
})
return
}
}
}
if luciferState.Color != nil && luciferState.Color.XY != nil {
newX := luciferState.Color.XY.X
newY := luciferState.Color.XY.Y
diffX := math.Abs(newX - currX)
diffY := math.Abs(newY - currY)
if diffX >= 0.0001 || diffY >= 0.0001 {
changed = true
xInt := int(math.Round(newX * 65535))
yInt := int(math.Round(newY * 65535))
_, err := b.client.PutDeviceColor(ikeaDevice.DeviceId, xInt, yInt)
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: id,
Error: "Failed to update color state",
})
return
}
}
}
if luciferState.Power != nil {
newPower := *luciferState.Power
diffPower := newPower != currPower
if diffPower {
changed = true
powerInt := 0
if newPower {
powerInt = 1
}
_, err := b.client.PutDevicePower(ikeaDevice.DeviceId, powerInt)
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: id,
Error: "Failed to update power state",
})
return
}
}
}
if changed {
bus.RunEvent(events.HardwareState{
ID: id,
InternalName: ikeaDevice.Name,
SupportFlags: defaultSF,
ColorFlags: defaultCF,
State: luciferState,
})
}
}
}
const defaultSF = device.SFlagPower | device.SFlagIntensity | device.SFlagColor
const defaultCF = device.CFlagXY
func catchPanic(mutexes ...sync.Locker) (retErr error) {
if err, ok := recover().(error); ok {
retErr = err
}
for _, mx := range mutexes {
mx.Unlock()
}
return
}
+118
View File
@@ -0,0 +1,118 @@
package tradfri
import (
"fmt"
lucifer3 "git.aiterp.net/lucifer3/server"
"git.aiterp.net/lucifer3/server/commands"
"git.aiterp.net/lucifer3/server/events"
"github.com/sirupsen/logrus"
"io"
"strings"
"sync"
)
func NewService() lucifer3.ActiveService {
logrus.StandardLogger().Out = io.Discard
logrus.StandardLogger().ExitFunc = func(i int) {
panic(fmt.Sprintf("tradfri: exit code %d", i))
}
return &service{
bridges: make(map[string]*Bridge, 4),
}
}
type service struct {
mu sync.Mutex
bridges map[string]*Bridge
}
func (s *service) Active() bool {
return true
}
func (s *service) HandleEvent(*lucifer3.EventBus, lucifer3.Event) {
// NOP
}
func (s *service) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) {
defer s.mu.Unlock()
s.mu.Lock()
switch command := command.(type) {
case commands.PairDevice:
if sub, ok := command.Matches("tradfri"); ok {
bridge, err := connect(sub, command.APIKey)
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: command.ID,
Error: err.Error(),
})
return
}
s.bridges[bridge.id] = bridge
bus.RunEvent(events.DeviceAccepted{
ID: bridge.id,
APIKey: bridge.newCredentials,
})
}
case commands.SearchDevices:
bus.RunEvent(events.DeviceFailed{
ID: command.ID,
Error: "Please follow instructions in IKEA app; they will appear here after pairing.",
})
case commands.SetState:
if _, ok := command.Matches("tradfri"); ok {
bridge, ok := s.findBridge(command.ID)
if !ok {
return
}
bridge.writeState(command.ID, command.State, bus)
}
case commands.SetStateBatch:
for id, state := range command {
if strings.HasPrefix(id, "tradfri") {
bridge, ok := s.findBridge(id)
if !ok {
return
}
bridge.writeState(id, state, bus)
}
}
case commands.ConnectDevice:
if sub, ok := command.Matches("tradfri"); ok {
if s.bridges[command.ID] == nil {
bridge, err := connect(sub, command.APIKey)
if err != nil {
bus.RunEvent(events.DeviceFailed{
ID: command.ID,
Error: err.Error(),
})
return
}
s.bridges[command.ID] = bridge
}
bus.RunEvent(events.DeviceConnected{Prefix: s.bridges[command.ID].id})
s.bridges[command.ID].listen(bus)
}
}
}
func (s *service) findBridge(id string) (*Bridge, bool) {
bits := strings.Split(id, ":")
for i := 2; i < len(bits); i++ {
prefix := strings.Join(bits[0:i], ":")
if s.bridges[prefix] != nil {
return s.bridges[prefix], true
}
}
return nil, false
}