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.
254 lines
6.0 KiB
254 lines
6.0 KiB
package hue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"git.aiterp.net/lucifer/new-server/models"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
type hueLightState struct {
|
|
index int
|
|
uniqueID string
|
|
externalID int
|
|
info LightData
|
|
input LightStateInput
|
|
|
|
stale bool
|
|
}
|
|
|
|
func (s *hueLightState) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(struct {
|
|
Index int `json:"index"`
|
|
UniqueID string `json:"uniqueId"`
|
|
ExternalID int `json:"externalId"`
|
|
Info LightData `json:"info"`
|
|
Input LightStateInput `json:"input"`
|
|
Stale bool `json:"stale"`
|
|
}{
|
|
Index: s.index,
|
|
UniqueID: s.uniqueID,
|
|
ExternalID: s.externalID,
|
|
Info: s.info,
|
|
Input: s.input,
|
|
Stale: s.stale,
|
|
})
|
|
}
|
|
|
|
func (s *hueLightState) Update(state models.DeviceState) {
|
|
input := LightStateInput{}
|
|
if state.Power {
|
|
input.On = ptrBool(true)
|
|
if state.Color.IsKelvin() {
|
|
input.CT = ptrInt(1000000 / *state.Color.K)
|
|
if *input.CT < s.info.Capabilities.Control.CT.Min {
|
|
*input.CT = s.info.Capabilities.Control.CT.Min
|
|
}
|
|
if *input.CT > s.info.Capabilities.Control.CT.Max {
|
|
*input.CT = s.info.Capabilities.Control.CT.Max
|
|
}
|
|
|
|
if s.input.CT == nil || *s.input.CT != *input.CT {
|
|
s.stale = true
|
|
}
|
|
} else if color, ok := state.Color.ToHS(); ok {
|
|
input.Hue = ptrInt(int(color.HS.Hue*(65536/360)) % 65536)
|
|
if s.input.Hue == nil || *s.input.Hue != *input.Hue {
|
|
s.stale = true
|
|
}
|
|
|
|
input.Sat = ptrInt(int(color.HS.Sat * 255))
|
|
if *input.Sat > 254 {
|
|
*input.Sat = 254
|
|
}
|
|
if *input.Sat < 0 {
|
|
*input.Sat = 0
|
|
}
|
|
if s.input.Sat == nil || *s.input.Sat != *input.Sat {
|
|
s.stale = true
|
|
}
|
|
}
|
|
|
|
input.Bri = ptrInt(int(state.Intensity * 255))
|
|
if *input.Bri > 254 {
|
|
*input.Bri = 254
|
|
} else if *input.Bri < 0 {
|
|
*input.Bri = 0
|
|
}
|
|
|
|
if s.input.Bri == nil || *s.input.Bri != *input.Bri {
|
|
s.stale = true
|
|
}
|
|
} else {
|
|
input.On = ptrBool(false)
|
|
}
|
|
|
|
if s.input.On == nil || *s.input.On != *input.On {
|
|
s.stale = true
|
|
}
|
|
|
|
input.TransitionTime = ptrInt(1)
|
|
|
|
s.input = input
|
|
}
|
|
|
|
func (s *hueLightState) CheckStaleness(state LightState) {
|
|
if s.input.On == nil || state.On != *s.input.On {
|
|
s.stale = true
|
|
}
|
|
if !state.On {
|
|
return
|
|
}
|
|
|
|
if state.ColorMode == "xy" {
|
|
s.stale = true
|
|
if s.input.CT == nil && s.input.Hue == nil {
|
|
s.input.Hue = ptrInt(state.Hue)
|
|
s.input.Sat = ptrInt(state.Sat)
|
|
s.input.Bri = ptrInt(state.Bri)
|
|
}
|
|
return
|
|
} else if state.ColorMode == "ct" {
|
|
if s.input.CT == nil || state.CT != *s.input.CT {
|
|
s.stale = true
|
|
}
|
|
} else {
|
|
if s.input.Hue == nil || state.Hue != *s.input.Hue || s.input.Sat == nil || state.Sat != *s.input.Sat {
|
|
s.stale = true
|
|
}
|
|
}
|
|
}
|
|
|
|
type hueSensorState struct {
|
|
index int
|
|
externalID int
|
|
uniqueID string
|
|
prevData *SensorData
|
|
prevTime time.Time
|
|
presenceCooldown int
|
|
}
|
|
|
|
func (state *hueSensorState) Update(newData SensorData) *models.Event {
|
|
stateTime, err := time.ParseInLocation("2006-01-02T15:04:05", newData.State.LastUpdated, time.UTC)
|
|
if err != nil {
|
|
// Invalid time is probably "none".
|
|
return nil
|
|
}
|
|
|
|
defer func() {
|
|
state.prevData = &newData
|
|
state.prevTime = stateTime
|
|
}()
|
|
|
|
if state.uniqueID != newData.UniqueID {
|
|
state.uniqueID = newData.UniqueID
|
|
}
|
|
|
|
if state.prevData != nil && newData.Type != state.prevData.Type {
|
|
return nil
|
|
}
|
|
|
|
switch newData.Type {
|
|
case "ZLLSwitch":
|
|
{
|
|
// Ignore old events.
|
|
if time.Since(stateTime) > time.Second*3 {
|
|
return nil
|
|
}
|
|
if state.prevData == nil {
|
|
return nil
|
|
}
|
|
|
|
pe := state.prevData.State.ButtonEvent
|
|
ce := newData.State.ButtonEvent
|
|
|
|
td := stateTime.Sub(state.prevTime) >= time.Second
|
|
pIdx := (pe / 1000) - 1
|
|
cIdx := (ce / 1000) - 1
|
|
pBtn := pe % 1000 / 2 // 0 = pressed, 1 = released
|
|
cBtn := ce % 1000 / 2 // 0 = pressed, 1 = released
|
|
|
|
if pIdx == cIdx && cBtn == 1 && pBtn == 0 {
|
|
// Do not allow 4002 after 4000.
|
|
// 4000 after 4002 is fine, though.
|
|
break
|
|
}
|
|
|
|
if cBtn != pBtn || cIdx != pIdx || td {
|
|
return &models.Event{
|
|
Name: models.ENButtonPressed,
|
|
Payload: map[string]string{
|
|
"buttonIndex": strconv.Itoa(cIdx),
|
|
"buttonName": buttonNames[cIdx],
|
|
"deviceId": strconv.Itoa(state.externalID),
|
|
"hueButtonEvent": strconv.Itoa(ce),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
case "ZLLPresence":
|
|
{
|
|
if state.prevData != nil && state.prevData.State.Presence != newData.State.Presence {
|
|
if newData.State.Presence {
|
|
state.presenceCooldown = -1
|
|
|
|
return &models.Event{
|
|
Name: models.ENSensorPresenceStarted,
|
|
Payload: map[string]string{
|
|
"deviceId": strconv.Itoa(state.externalID),
|
|
"deviceInternalId": newData.UniqueID,
|
|
},
|
|
}
|
|
} else {
|
|
state.presenceCooldown = 0
|
|
}
|
|
}
|
|
|
|
if state.presenceCooldown == -2 {
|
|
state.presenceCooldown = int(time.Since(stateTime) / time.Minute)
|
|
}
|
|
|
|
nextEventWait := time.Minute * time.Duration(state.presenceCooldown)
|
|
if state.presenceCooldown != -1 && !newData.State.Presence && time.Since(stateTime) > nextEventWait {
|
|
state.presenceCooldown += 1
|
|
|
|
return &models.Event{
|
|
Name: models.ENSensorPresenceEnded,
|
|
Payload: map[string]string{
|
|
"deviceId": strconv.Itoa(state.externalID),
|
|
"deviceInternalId": newData.UniqueID,
|
|
"minutesElapsed": strconv.Itoa(state.presenceCooldown - 1),
|
|
"secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60),
|
|
"lastUpdated": strconv.FormatInt(stateTime.Unix(), 10),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
case "ZLLTemperature":
|
|
if time.Since(stateTime) > (time.Minute * 15) {
|
|
return nil
|
|
}
|
|
|
|
if !state.prevTime.Equal(stateTime) {
|
|
return &models.Event{
|
|
Name: models.ENSensorTemperature,
|
|
Payload: map[string]string{
|
|
"temperature": strconv.FormatFloat(float64(newData.State.Temperature)/100, 'f', 2, 64),
|
|
"deviceId": strconv.Itoa(state.externalID),
|
|
"deviceInternalId": newData.UniqueID,
|
|
"lastUpdated": strconv.FormatInt(stateTime.Unix(), 10),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ptrBool(v bool) *bool {
|
|
return &v
|
|
}
|
|
|
|
func ptrInt(v int) *int {
|
|
return &v
|
|
}
|