You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
5.4 KiB
246 lines
5.4 KiB
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
|
|
}
|