5 Commits
Author SHA1 Message Date
Stian Fredrik Aune 8c9b47aa1d Add temperature to API.
continuous-integration/drone/push Build is passing
2021-11-07 19:03:42 +01:00
Stian Fredrik Aune 5bd7a7f6b6 cleanup and fix
continuous-integration/drone/push Build is passing
2021-11-07 18:56:21 +01:00
Stian Fredrik Aune 88bd4dc073 Mill. Take 1.
continuous-integration/drone/push Build is failing
2021-11-07 18:38:32 +01:00
gisle 6443e0ecd3 any/all.scene=-1 now checks for no scene.
continuous-integration/drone/push Build is passing
2021-10-31 09:51:06 +01:00
gisle 147231e4fd add sensor scenes for Motion and Temperature.
continuous-integration/drone/push Build is passing
2021-10-30 14:38:14 +02:00
25 changed files with 628 additions and 60 deletions
-9
View File
@@ -4,15 +4,6 @@ kind: pipeline
type: docker
steps:
- name: go-build
image: golang:1.17
commands:
- go test -v ./...
when:
event:
exclude:
- tag
- name: go-test
image: golang:1.17
commands:
+1 -1
View File
@@ -52,7 +52,7 @@ func Bridges(r gin.IRoutes) {
return []models.Bridge{bridge}, nil
}
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.DryRun)
bridges, err := driver.SearchBridge(ctxOf(c), body.Address, body.Token, body.DryRun)
if err != nil {
return nil, err
}
+2
View File
@@ -4,6 +4,7 @@ import (
"git.aiterp.net/lucifer/new-server/internal/drivers"
"git.aiterp.net/lucifer/new-server/internal/drivers/hue"
"git.aiterp.net/lucifer/new-server/internal/drivers/lifx"
"git.aiterp.net/lucifer/new-server/internal/drivers/mill"
"git.aiterp.net/lucifer/new-server/internal/drivers/nanoleaf"
"git.aiterp.net/lucifer/new-server/models"
"sync"
@@ -18,6 +19,7 @@ func DriverProvider() models.DriverProvider {
models.DTNanoLeaf: &nanoleaf.Driver{},
models.DTHue: &hue.Driver{},
models.DTLIFX: &lifx.Driver{},
models.DTMill: &mill.Driver{},
}
})
+25 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/app/config"
"git.aiterp.net/lucifer/new-server/app/services/publisher"
"git.aiterp.net/lucifer/new-server/models"
"log"
"math"
@@ -66,7 +67,6 @@ func handleEvent(event models.Event) (responses []models.Event) {
err := handleSpecial(event)
if err != nil {
log.Printf("Special event handler error (%s): %v", event.Name, err)
return
}
paramStrings := make([]string, 0, 8)
@@ -220,7 +220,30 @@ func handleSpecial(event models.Event) error {
}
return driver.Publish(ctx, bridge, devices)
case models.ENSensorPresenceStarted, models.ENSensorPresenceEnded, models.ENSensorTemperature:
id, _ := strconv.Atoi(event.Payload["deviceId"])
updateTime, _ := strconv.ParseInt(event.Payload["lastUpdated"], 10, 64)
state := models.SceneSensor{
ID: id,
UpdateTime: time.Unix(updateTime, 0),
}
switch event.Name {
case models.ENSensorPresenceStarted:
presence := true
state.Presence = &presence
case models.ENSensorPresenceEnded:
presence := false
state.Presence = &presence
case models.ENSensorTemperature:
temperature, _ := strconv.ParseFloat(event.Payload["temperature"], 64)
state.Temperature = &temperature
}
publisher.Global().UpdateSensor(state)
return nil
default:
return nil
}
}
}
+15
View File
@@ -17,6 +17,7 @@ type Publisher struct {
started map[int]bool
pending map[int][]models.Device
waiting map[int]chan struct{}
sensorCache map[int]models.SceneSensor
}
func (p *Publisher) SceneState(deviceID int) *models.DeviceState {
@@ -42,6 +43,15 @@ func (p *Publisher) UpdateScene(data models.Scene) {
p.mu.Unlock()
}
func (p *Publisher) UpdateSensor(data models.SceneSensor) {
p.mu.Lock()
if assignment := p.sceneAssignment[data.ID]; assignment != nil {
assignment.UpdateSensor(data)
}
p.sensorCache[data.ID] = data
p.mu.Unlock()
}
func (p *Publisher) ReloadScenes(ctx context.Context) error {
scenes, err := config.SceneRepository().FetchAll(ctx)
if err != nil {
@@ -90,6 +100,10 @@ func (p *Publisher) Publish(devices ...models.Device) {
if p.sceneAssignment[device.ID] != nil {
p.sceneAssignment[device.ID].UpsertDevice(device)
if cache, ok := p.sensorCache[device.ID]; ok {
p.sceneAssignment[device.ID].UpdateSensor(cache)
}
} else {
p.pending[device.BridgeID] = append(p.pending[device.BridgeID], device)
if p.waiting[device.BridgeID] != nil {
@@ -291,6 +305,7 @@ var publisher = Publisher{
started: make(map[int]bool),
pending: make(map[int][]models.Device),
waiting: make(map[int]chan struct{}),
sensorCache: make(map[int]models.SceneSensor),
}
func Global() *Publisher {
+41 -2
View File
@@ -13,11 +13,25 @@ type Scene struct {
roleMap map[int]int
roleList map[int][]models.Device
lastStates map[int]models.DeviceState
sensors []models.SceneSensor
due bool
lastInterval int64
}
func (s *Scene) UpdateSensor(data models.SceneSensor) {
s.due = true
for i, sensor := range s.sensors {
if sensor.ID == data.ID {
s.sensors[i] = data
return
}
}
s.sensors = append(s.sensors, data)
}
// UpdateScene updates the scene data and re-seats all devices.
func (s *Scene) UpdateScene(data models.Scene) {
devices := make([]models.Device, 0, 16)
@@ -86,6 +100,13 @@ func (s *Scene) RemoveDevice(device models.Device) {
delete(s.roleMap, device.ID)
delete(s.lastStates, device.ID)
for i, sensor := range s.sensors {
if sensor.ID == device.ID {
s.sensors = append(s.sensors[:i], s.sensors[i+1:]...)
break
}
}
}
func (s *Scene) Empty() bool {
@@ -130,6 +151,7 @@ func (s *Scene) Run() []models.Device {
return []models.Device{}
}
currentTime := time.Now()
intervalNumber := int64(0)
intervalMax := int64(1)
if s.data.IntervalMS > 0 {
@@ -149,14 +171,31 @@ func (s *Scene) Run() []models.Device {
continue
}
sensorCount := 0
for _, device := range list {
if device.IsOnlySensor() {
sensorCount += 1
}
}
role := s.data.Roles[i]
jSkip := 0
for j, device := range list {
if device.IsOnlySensor() {
updatedDevices = append(updatedDevices, device)
jSkip += 1
continue
}
newState := role.ApplyEffect(&device, models.SceneRunContext{
Index: j,
Length: len(list),
CurrentTime: currentTime,
Index: j - jSkip,
Length: len(list) - sensorCount,
IntervalNumber: intervalNumber,
IntervalMax: intervalMax,
Sensors: s.sensors,
})
err := device.SetState(newState)
+1 -1
View File
@@ -33,7 +33,7 @@ func main() {
}
// Find bridge
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, !*flagPair)
bridges, err := driver.SearchBridge(context.Background(), *flagAddress, *flagToken, !*flagPair)
if err != nil {
log.Fatalln("Failed to search bridge:", err)
}
+1
View File
@@ -157,6 +157,7 @@ func applyCmdToHandler(model models.EventHandler, cmd Command) models.EventHandl
model.Actions.SetPower = nds.Power
model.Actions.SetColor = nds.Color
model.Actions.SetIntensity = nds.Intensity
model.Actions.SetTemperature = nds.Temperature
// Add intensity
model.Actions.AddIntensity = cmd.Params.Get("add-intensity").Float()
+1 -1
View File
@@ -44,7 +44,7 @@ func WriteDeviceStateTable(w io.Writer, devices []models.Device) {
temperatureString := ""
if v.HasCapability(models.DCTemperatureControl, models.DCTemperatureSensor) {
temperatureString = strconv.FormatFloat(v.State.Temperature, 'f', -1, 64)
temperatureString = fmt.Sprintf("%d", v.State.Temperature)
}
intensityString := ""
+1 -1
View File
@@ -19,7 +19,7 @@ type Driver struct {
bridges []*Bridge
}
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
if address == "" {
if !dryRun {
return nil, models.ErrAddressOnlyDryRunnable
+2
View File
@@ -219,6 +219,7 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
"deviceInternalId": newData.UniqueID,
"minutesElapsed": strconv.Itoa(state.presenceCooldown - 1),
"secondsElapsed": strconv.Itoa((state.presenceCooldown - 1) * 60),
"lastUpdated": strconv.FormatInt(stateTime.Unix(), 10),
},
}
}
@@ -235,6 +236,7 @@ func (state *hueSensorState) Update(newData SensorData) *models.Event {
"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),
},
}
}
+1 -1
View File
@@ -14,7 +14,7 @@ type Driver struct {
bridges []*Bridge
}
func (d *Driver) SearchBridge(ctx context.Context, address string, _ bool) ([]models.Bridge, error) {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, _ bool) ([]models.Bridge, error) {
if address == "" {
ifaces, err := net.Interfaces()
if err != nil {
+240
View File
@@ -0,0 +1,240 @@
package mill
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha1"
"encoding/json"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"io"
"log"
"net/http"
"strconv"
"sync"
"time"
)
type bridge struct {
mu sync.Mutex
luciferID int
username string
password string
token string
userId int
mustRefreshBy time.Time
luciferMillIDMap map[int]int
millLuciferIDMap map[int]int
}
func (b *bridge) listDevices(ctx context.Context) ([]models.Device, error) {
err := b.authenticate(ctx)
if err != nil {
return nil, err
}
var shlRes listHomeResBody
err = b.command(ctx, "selectHomeList", listHomeReqBody{}, &shlRes)
if err != nil {
return nil, err
}
devices := make([]millDevice, 0, 16)
for _, home := range shlRes.HomeList {
var gidRes listDeviceResBody
err = b.command(ctx, "getIndependentDevices", listDeviceReqBody{HomeID: home.HomeID}, &gidRes)
if err != nil {
return nil, err
}
devices = append(devices, gidRes.DeviceInfo...)
}
luciferDevices := make([]models.Device, len(devices), len(devices))
for i, device := range devices {
luciferDevices[i] = models.Device{
ID: b.millLuciferIDMap[device.DeviceID],
BridgeID: b.luciferID,
InternalID: fmt.Sprintf("%d", device.DeviceID),
Icon: "heater",
Name: device.DeviceName,
Capabilities: []models.DeviceCapability{models.DCTemperatureControl, models.DCPower},
ButtonNames: nil,
DriverProperties: map[string]interface{}{
"subDomain": fmt.Sprintf("%d", device.SubDomainID),
},
UserProperties: nil,
SceneAssignments: nil,
SceneState: nil,
State: models.DeviceState{
Power: device.PowerStatus > 0,
Temperature: device.HolidayTemp,
},
Tags: nil,
}
}
return luciferDevices, nil
}
func (b *bridge) pushStateChange(ctx context.Context, deviceModel models.Device) error {
b.mu.Lock()
if b.luciferMillIDMap == nil {
b.luciferMillIDMap = make(map[int]int, 4)
b.millLuciferIDMap = make(map[int]int, 4)
}
if b.luciferMillIDMap[deviceModel.ID] == 0 {
millID, _ := strconv.Atoi(deviceModel.InternalID)
b.luciferMillIDMap[deviceModel.ID] = millID
b.millLuciferIDMap[millID] = deviceModel.ID
}
b.mu.Unlock()
status := 0
if deviceModel.State.Power {
status = 1
}
powerReq := deviceControlReqBody{
SubDomain: deviceModel.DriverProperties["subDomain"].(string),
DeviceID: b.luciferMillIDMap[deviceModel.ID],
TestStatus: 1,
Status: status,
}
err := b.command(ctx, "deviceControl", powerReq, nil)
if err != nil {
return err
}
tempReq := changeInfoReqBody{
DeviceID: b.luciferMillIDMap[deviceModel.ID],
Value: deviceModel.State.Temperature,
TimeZoneNum: "+02:00",
Key: "holidayTemp",
}
err = b.command(ctx, "changeDeviceInfo", tempReq, nil)
if err != nil {
return err
}
return nil
}
func (b *bridge) command(ctx context.Context, command string, payload interface{}, target interface{}) error {
err := b.authenticate(ctx)
if err != nil {
return err
}
url := serviceEndpoint + command
method := "POST"
nonce := makeNonce()
timestamp := fmt.Sprintf("%d", time.Now().Unix())
timeout := "300"
h := sha1.New()
h.Write([]byte(timeout))
h.Write([]byte(timestamp))
h.Write([]byte(nonce))
h.Write([]byte(b.token))
signature := fmt.Sprintf("%x", h.Sum(nil))
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body))
if err != nil {
return err
}
addDefaultHeaders(req)
req.Header.Add("X-Zc-Timestamp", timestamp)
req.Header.Add("X-Zc-Timeout", timeout)
req.Header.Add("X-Zc-Nonce", nonce)
req.Header.Add("X-Zc-User-Id", fmt.Sprintf("%d", b.userId))
req.Header.Add("X-Zc-User-Signature", signature)
req.Header.Add("X-Zc-Content-Length", fmt.Sprintf("%d", len(body)))
res, err := http.DefaultClient.Do(req)
if err != nil {
return models.ErrCannotForwardRequest
} else if res.StatusCode != 200 {
return models.ErrIncorrectToken
}
if target == nil {
return nil
}
err = json.NewDecoder(res.Body).Decode(&target)
if err != nil {
return models.ErrUnexpectedResponse
}
return nil
}
func (b *bridge) authenticate(ctx context.Context) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) {
body, err := json.Marshal(authReqBody{
Account: b.username,
Password: b.password,
})
if err != nil {
return models.ErrMissingToken
}
req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint + "login", bytes.NewReader(body))
if err != nil {
return models.ErrMissingToken
}
addDefaultHeaders(req)
res, err := http.DefaultClient.Do(req)
if err != nil {
return models.ErrCannotForwardRequest
} else if res.StatusCode != 200 {
return models.ErrIncorrectToken
}
var resBody authResBody
err = json.NewDecoder(res.Body).Decode(&resBody)
if err != nil {
return models.ErrBridgeSearchFailed
}
log.Printf("Mill: Authenticated as %s", resBody.NickName)
b.userId = resBody.UserID
b.token = resBody.Token
b.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location)
}
return nil
}
func makeNonce() string {
buf := make([]byte, 8)
_, _ = io.ReadFull(rand.Reader, buf)
return fmt.Sprintf("%x", buf)
}
func addDefaultHeaders(req *http.Request) {
req.Header.Add("Content-Type", "application/x-zc-object")
req.Header.Add("Connection", "Keep-Alive")
req.Header.Add("X-Zc-Major-Domain", "seanywell")
req.Header.Add("X-Zc-Msg-Name", "millService")
req.Header.Add("X-Zc-Sub-Domain", "milltype")
req.Header.Add("X-Zc-Seq-Id", "1")
req.Header.Add("X-Zc-Version", "1")
}
+100
View File
@@ -0,0 +1,100 @@
package mill
import (
"context"
"fmt"
"git.aiterp.net/lucifer/new-server/models"
"sync"
"time"
)
type Driver struct {
mu sync.Mutex
bridges map[int]*bridge
}
func (d *Driver) SearchBridge(ctx context.Context, address, token string, _ bool) ([]models.Bridge, error) {
bridgeData := models.Bridge{
Name: fmt.Sprintf("Mill account (%s)", address),
Driver: models.DTMill,
Address: address,
Token: token,
}
b, err := d.ensureBridge(bridgeData)
if err != nil {
return nil, err
}
err = b.authenticate(ctx)
if err != nil {
return nil, err
}
return []models.Bridge{bridgeData}, nil
}
func (d *Driver) SearchDevices(context.Context, models.Bridge, time.Duration) ([]models.Device, error) {
// You would have to configure devices with the Mill app, unfortunately.
return []models.Device{}, nil
}
func (d *Driver) ListDevices(ctx context.Context, bridge models.Bridge) ([]models.Device, error) {
b, err := d.ensureBridge(bridge)
if err != nil {
return nil, err
}
return b.listDevices(ctx)
}
func (d *Driver) Publish(ctx context.Context, bridge models.Bridge, devices []models.Device) error {
b, err := d.ensureBridge(bridge)
if err != nil {
return err
}
for _, device := range devices {
err = b.pushStateChange(ctx, device)
if err != nil {
return err
}
}
return nil
}
func (d *Driver) Run(ctx context.Context, _ models.Bridge, _ chan<- models.Event) error {
// TODO: Maybe do something with the thermostat on the device
for {
select {
case <-ctx.Done():
return ctx.Err()
}
}
}
func (d *Driver) ensureBridge(model models.Bridge) (*bridge, error) {
d.mu.Lock()
defer d.mu.Unlock()
if d.bridges == nil {
d.bridges = make(map[int]*bridge, 4)
}
if d.bridges[model.ID] == nil {
newBridge := &bridge{
luciferID: model.ID,
username: model.Address,
password: model.Token,
}
if model.ID <= 0 {
return newBridge, nil
}
d.bridges[model.ID] = newBridge
}
return d.bridges[model.ID], nil
}
+77
View File
@@ -0,0 +1,77 @@
package mill
import "time"
const accountEndpoint = "https://eurouter.ablecloud.cn:9005/zc-account/v1/"
const serviceEndpoint = "https://eurouter.ablecloud.cn:9005/millService/v1/"
type millHome struct {
HomeID int64 `json:"homeId"`
HomeName string `json:"homeName"`
}
type millDevice struct {
DeviceID int `json:"deviceId"`
DeviceName string `json:"deviceName"`
PowerStatus int `json:"powerStatus"`
HolidayTemp int `json:"holidayTemp"`
CurrentTemp float64 `json:"currentTemp"`
SubDomainID int `json:"subDomainId"`
}
type authReqBody struct {
Account string `json:"account"`
Password string `json:"password"`
}
type authResBody struct {
Token string `json:"token"`
UserID int `json:"userId"`
NickName string `json:"nickName"`
TokenExpire string `json:"tokenExpire"`
}
type listHomeReqBody struct{}
type listHomeResBody struct {
HomeList []millHome `json:"homeList"`
}
type listDeviceReqBody struct {
HomeID int64 `json:"homeId"`
}
type listDeviceResBody struct {
DeviceInfo []millDevice `json:"deviceInfo"`
}
type changeInfoReqBody struct {
HomeType int `json:"homeType"`
DeviceID int `json:"deviceId"`
Value int `json:"value"`
TimeZoneNum string `json:"timeZoneNum"`
Key string `json:"key"`
}
type deviceControlReqBody struct {
SubDomain string `json:"subDomain"`
DeviceID int `json:"deviceId"`
TestStatus int `json:"testStatus"`
Operation int `json:"operation"`
Status int `json:"status"`
WindStatus int `json:"windStatus"`
TempType int `json:"tempType"`
PowerLevel int `json:"powerLevel"`
}
var location *time.Location
func init() {
myLocation, err := time.LoadLocation("Europe/Oslo")
if err != nil {
panic(err.Error())
}
location = myLocation
}
+1 -1
View File
@@ -17,7 +17,7 @@ type Driver struct {
// SearchBridge checks the bridge at the address. If it's not a dry-run, you must hold down the power button
// before calling this function and wait for the pattern.
func (d *Driver) SearchBridge(ctx context.Context, address string, dryRun bool) ([]models.Bridge, error) {
func (d *Driver) SearchBridge(ctx context.Context, address, _ string, dryRun bool) ([]models.Bridge, error) {
res, err := http.Get(fmt.Sprintf("http://%s/device_info", address))
if err != nil {
return nil, err
+17 -14
View File
@@ -23,12 +23,13 @@ type deviceRecord struct {
}
type deviceStateRecord struct {
DeviceID int `db:"device_id"`
Hue float64 `db:"hue"`
Saturation float64 `db:"saturation"`
Kelvin int `db:"kelvin"`
Power bool `db:"power"`
Intensity float64 `db:"intensity"`
DeviceID int `db:"device_id"`
Hue float64 `db:"hue"`
Saturation float64 `db:"saturation"`
Kelvin int `db:"kelvin"`
Power bool `db:"power"`
Intensity float64 `db:"intensity"`
Temperature int `db:"temperature"`
}
type devicePropertyRecord struct {
@@ -227,15 +228,16 @@ func (r *DeviceRepo) SaveMany(ctx context.Context, mode models.SaveMode, devices
if mode == 0 || mode&models.SMState != 0 {
_, err = tx.NamedExecContext(ctx, `
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity)
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity)
REPLACE INTO device_state(device_id, hue, saturation, kelvin, power, intensity, temperature)
VALUES (:device_id, :hue, :saturation, :kelvin, :power, :intensity, :temperature)
`, deviceStateRecord{
DeviceID: record.ID,
Hue: device.State.Color.Hue,
Saturation: device.State.Color.Saturation,
Kelvin: device.State.Color.Kelvin,
Power: device.State.Power,
Intensity: device.State.Intensity,
DeviceID: record.ID,
Hue: device.State.Color.Hue,
Saturation: device.State.Color.Saturation,
Kelvin: device.State.Color.Kelvin,
Power: device.State.Power,
Intensity: device.State.Intensity,
Temperature: device.State.Temperature,
})
if err != nil {
return dbErr(err)
@@ -379,6 +381,7 @@ func (r *DeviceRepo) populate(ctx context.Context, records []deviceRecord) ([]mo
Kelvin: state.Kelvin,
},
Intensity: state.Intensity,
Temperature: state.Temperature,
}
}
}
+1
View File
@@ -23,6 +23,7 @@ var (
DTHue DriverKind = "Hue"
DTNanoLeaf DriverKind = "Nanoleaf"
DTLIFX DriverKind = "LIFX"
DTMill DriverKind = "Mill"
)
var ValidDriverKinds = []DriverKind{
+16 -10
View File
@@ -2,7 +2,6 @@ package models
import (
"context"
"math"
"strings"
"time"
)
@@ -38,7 +37,7 @@ type DeviceState struct {
Power bool `json:"power"`
Color ColorValue `json:"color,omitempty"`
Intensity float64 `json:"intensity,omitempty"`
Temperature float64 `json:"temperature"`
Temperature int `json:"temperature"`
}
type DeviceScene struct {
@@ -156,6 +155,17 @@ func (d *Device) HasTag(tags ...string) bool {
return false
}
func (d *Device) IsOnlySensor() bool {
return !d.HasCapability(
DCPower,
DCColorHS,
DCColorHSK,
DCColorKelvin,
DCIntensity,
DCTemperatureControl,
)
}
func (d *Device) HasCapability(capabilities ...DeviceCapability) bool {
for _, c := range d.Capabilities {
for _, c2 := range capabilities {
@@ -188,6 +198,10 @@ func (d *Device) SetState(newState NewDeviceState) error {
d.State.Intensity = *newState.Intensity
}
if newState.Temperature != nil && d.HasCapability(DCTemperatureControl) {
d.State.Temperature = *newState.Temperature
}
return nil
}
@@ -198,14 +212,6 @@ func (s *NewDeviceState) RelativeTo(device Device) NewDeviceState {
intensity := device.State.Intensity * *s.Intensity
n.Intensity = &intensity
}
if s.Color != nil {
c, err := ParseColorValue(*s.Color)
if err == nil {
c.Hue = math.Mod(device.State.Color.Hue+c.Hue, 360)
c.Saturation *= device.State.Color.Saturation
c.Kelvin += device.State.Color.Kelvin
}
}
return n
}
+1 -1
View File
@@ -10,7 +10,7 @@ type DriverProvider interface {
}
type Driver interface {
SearchBridge(ctx context.Context, address string, dryRun bool) ([]Bridge, error)
SearchBridge(ctx context.Context, address, token string, dryRun bool) ([]Bridge, error)
SearchDevices(ctx context.Context, bridge Bridge, timeout time.Duration) ([]Device, error)
ListDevices(ctx context.Context, bridge Bridge) ([]Device, error)
Publish(ctx context.Context, bridge Bridge, devices []Device) error
+1
View File
@@ -15,6 +15,7 @@ var ErrIncorrectToken = errors.New("driver is not accepting authentication infor
var ErrUnexpectedResponse = errors.New("driver api returned unexpected response (wrong driver selected?)")
var ErrBridgeSearchFailed = errors.New("bridge search failed")
var ErrAddressOnlyDryRunnable = errors.New("this address may only be used for a dry run")
var ErrCannotForwardRequest = errors.New("driver is not able to forward requests")
var ErrInvalidAddress = errors.New("invalid mac address")
var ErrPayloadTooShort = errors.New("payload too short")
-1
View File
@@ -24,7 +24,6 @@ var (
ENBridgeDisconnected = "BridgeDisconnected"
ENButtonPressed = "ButtonPressed"
ENSensorPresenceStarted = "SensorPresenceStarted"
ENSensorPresenceEnding = "SensorPresenceEnding"
ENSensorPresenceEnded = "SensorPresenceEnded"
ENSensorTemperature = "SensorTemperature"
)
+1 -5
View File
@@ -168,12 +168,8 @@ func (c *EventCondition) checkDevice(key string, device Device) (matches bool, s
return false, true
}
return c.matches(strconv.FormatFloat(device.State.Temperature, 'f', -1, 64)), false
return c.matches(strconv.Itoa(device.State.Temperature)), false
case "scene":
if len(device.SceneAssignments) == 0 {
return false, false
}
sceneId := -1
for _, assignment := range device.SceneAssignments {
duration := time.Duration(assignment.DurationMS) * time.Millisecond
+71 -10
View File
@@ -61,24 +61,38 @@ const (
SEGradient SceneEffect = "Gradient"
SEWalkingGradient SceneEffect = "WalkingGradient"
SETransition SceneEffect = "Transition"
SEMotion SceneEffect = "Motion"
SETemperature SceneEffect = "Temperature"
)
type SceneRole struct {
Effect SceneEffect `json:"effect"`
PowerMode ScenePowerMode `json:"powerMode"`
TargetKind ReferenceKind `json:"targetKind"`
TargetValue string `json:"targetValue"`
Interpolate bool `json:"interpolate"`
Relative bool `json:"relative"`
Order string `json:"order"`
States []NewDeviceState `json:"states"`
Effect SceneEffect `json:"effect"`
MotionSeconds float64 `json:"motionSeconds"`
MinTemperature int `json:"minTemperature"`
MaxTemperature int `json:"maxTemperature"`
PowerMode ScenePowerMode `json:"powerMode"`
TargetKind ReferenceKind `json:"targetKind"`
TargetValue string `json:"targetValue"`
Interpolate bool `json:"interpolate"`
Relative bool `json:"relative"`
Order string `json:"order"`
States []NewDeviceState `json:"states"`
}
type SceneRunContext struct {
CurrentTime time.Time
Index int
Length int
IntervalNumber int64
IntervalMax int64
Sensors []SceneSensor
}
type SceneSensor struct {
ID int
UpdateTime time.Time
Temperature *float64
Presence *bool
}
func (d *SceneRunContext) PositionFacShifted() float64 {
@@ -120,7 +134,7 @@ func (r *SceneRole) Validate() error {
}
switch r.Effect {
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition:
case SEStatic, SERandom, SEGradient, SEWalkingGradient, SETransition, SEMotion, SETemperature:
default:
return ErrSceneRoleUnknownEffect
}
@@ -178,6 +192,53 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
newState = r.State(c.PositionFacShifted())
case SETransition:
newState = r.State(c.IntervalFac())
case SEMotion:
presence := false
absenceTime := time.Time{}
for _, sensors := range c.Sensors {
if sensors.Presence == nil {
continue
}
if *sensors.Presence {
presence = true
} else {
if sensors.UpdateTime.After(absenceTime) {
absenceTime = sensors.UpdateTime
}
}
}
if presence {
newState = r.State(0.0)
} else {
fac := c.CurrentTime.Sub(absenceTime).Seconds() / r.MotionSeconds
if fac < 0 {
fac = 0
} else if fac > 1 {
fac = 1
}
newState = r.State(fac)
}
case SETemperature:
avg := 0.0
count := 0
for _, sensor := range c.Sensors {
if sensor.Temperature == nil {
continue
}
if count == 0 {
avg = *sensor.Temperature
count = 1
} else {
avg = ((avg * float64(count)) + *sensor.Temperature) / float64(count+1)
count += 1
}
}
}
if r.Relative {
@@ -188,7 +249,7 @@ func (r *SceneRole) ApplyEffect(device *Device, c SceneRunContext) (newState New
case SPDevice:
newState.Power = nil
case SPScene:
// Do nothing
// Do nothing
case SPBoth:
if newState.Power != nil {
powerIntersection := *newState.Power && device.State.Power
@@ -0,0 +1,11 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE device_state
ADD COLUMN temperature INT NOT NULL DEFAULT 0;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE device_state
DROP COLUMN temperature;
-- +goose StatementEnd