Stian Fredrik Aune
2 years ago
7 changed files with 765 additions and 1 deletions
-
2cmd/bustest/main.go
-
90services/mill/bridge.go
-
365services/mill/online.go
-
120services/mill/onlineutils.go
-
145services/mill/service.go
-
39services/mill/wifi.go
-
5services/tradfri/bridge.go
@ -0,0 +1,90 @@ |
|||||
|
package mill |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
lucifer3 "git.aiterp.net/lucifer3/server" |
||||
|
"git.aiterp.net/lucifer3/server/device" |
||||
|
"git.aiterp.net/lucifer3/server/events" |
||||
|
"strings" |
||||
|
) |
||||
|
|
||||
|
type Bridge interface { |
||||
|
// SetBus to apply a bus that changes will be sent to
|
||||
|
SetBus(bus *lucifer3.EventBus) |
||||
|
|
||||
|
// SetState updates device state
|
||||
|
SetState(id string, state device.State) bool |
||||
|
|
||||
|
// Start triggers the event emitting part, including device discovery
|
||||
|
Start() |
||||
|
|
||||
|
// IsStarted returns true when it's running
|
||||
|
IsStarted() bool |
||||
|
} |
||||
|
|
||||
|
func MakeBridge(id, apiKey string) (Bridge, bool) { |
||||
|
parts := strings.Split(id, ":") |
||||
|
if len(parts) < 3 { |
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
driver := parts[0] |
||||
|
version := parts[1] |
||||
|
target := parts[2] |
||||
|
|
||||
|
if driver != "mill" { |
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
id = fmt.Sprintf("%s:%s:%s", driver, version, target) |
||||
|
switch version { |
||||
|
case "2": |
||||
|
return &OnlineBridge{ |
||||
|
ID: id, |
||||
|
Email: target, |
||||
|
Password: apiKey, |
||||
|
Gen2: true, |
||||
|
Gen3: false, |
||||
|
}, true |
||||
|
case "23": |
||||
|
return &OnlineBridge{ |
||||
|
ID: id, |
||||
|
Email: target, |
||||
|
Password: apiKey, |
||||
|
Gen2: true, |
||||
|
Gen3: true, |
||||
|
}, true |
||||
|
case "3": |
||||
|
if strings.Contains(target, "@") { |
||||
|
return &OnlineBridge{ |
||||
|
ID: id, |
||||
|
Email: target, |
||||
|
Password: apiKey, |
||||
|
Gen2: false, |
||||
|
Gen3: true, |
||||
|
}, true |
||||
|
} else { |
||||
|
return &WifiBridge{ |
||||
|
ID: id, |
||||
|
IP: target, |
||||
|
}, true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil, false |
||||
|
} |
||||
|
|
||||
|
func deviceFailed(id string, err any) events.DeviceFailed { |
||||
|
msg := "(no message)" |
||||
|
switch typed := err.(type) { |
||||
|
case string: |
||||
|
msg = typed |
||||
|
case error: |
||||
|
msg = typed.Error() |
||||
|
} |
||||
|
|
||||
|
return events.DeviceFailed{ |
||||
|
ID: id, |
||||
|
Error: msg, |
||||
|
} |
||||
|
} |
@ -0,0 +1,365 @@ |
|||||
|
package mill |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"crypto/sha1" |
||||
|
"encoding/json" |
||||
|
"errors" |
||||
|
"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/gentools" |
||||
|
"log" |
||||
|
"math" |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
"sync" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type OnlineBridge struct { |
||||
|
ID string |
||||
|
Email string |
||||
|
Password string |
||||
|
Gen2 bool |
||||
|
Gen3 bool |
||||
|
|
||||
|
genMap map[string]int |
||||
|
internalMap map[string]int |
||||
|
nameMap map[string]string |
||||
|
stateMap map[string]device.State |
||||
|
|
||||
|
mx sync.Mutex |
||||
|
started bool |
||||
|
bus *lucifer3.EventBus |
||||
|
token string |
||||
|
userId int |
||||
|
mustRefreshBy time.Time |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) SetBus(bus *lucifer3.EventBus) { |
||||
|
o.bus = bus |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) SetState(id string, state device.State) bool { |
||||
|
if !o.started { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if err := o.authenticate(); err != nil { |
||||
|
o.bus.RunEvent(deviceFailed(o.ID, err)) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
currState := o.stateMap[id] |
||||
|
|
||||
|
if state.Power != nil { |
||||
|
currState.Power = state.Power |
||||
|
} |
||||
|
if state.Temperature != nil { |
||||
|
currState.Temperature = state.Temperature |
||||
|
} |
||||
|
|
||||
|
o.stateMap[id] = currState |
||||
|
|
||||
|
if err := o.refresh(); err != nil { |
||||
|
o.bus.RunEvent(deviceFailed(o.ID, err)) |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) Start() { |
||||
|
o.genMap = make(map[string]int, 16) |
||||
|
o.internalMap = make(map[string]int, 16) |
||||
|
o.nameMap = make(map[string]string, 16) |
||||
|
o.stateMap = make(map[string]device.State, 16) |
||||
|
|
||||
|
if err := o.refresh(); err != nil { |
||||
|
o.bus.RunEvent(deviceFailed(o.ID, err)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
o.started = true |
||||
|
go func() { |
||||
|
defer func() { |
||||
|
log.Printf("Mill: %s stopped unexpectedly", o.ID) |
||||
|
o.started = false |
||||
|
}() |
||||
|
|
||||
|
for { |
||||
|
time.Sleep(5 * time.Minute) |
||||
|
|
||||
|
if err := o.refresh(); err != nil { |
||||
|
o.bus.RunEvent(deviceFailed(o.ID, err)) |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) IsStarted() bool { |
||||
|
return o.started |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) refresh() error { |
||||
|
var shlRes listHomeResBody |
||||
|
err := o.command("selectHomeList", listHomeReqBody{}, &shlRes) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
devices := make([]millDevice, 0, 16) |
||||
|
for _, home := range shlRes.HomeList { |
||||
|
var gidRes listDeviceResBody |
||||
|
err = o.command("getIndependentDevices", listDeviceReqBody{HomeID: home.HomeID}, &gidRes) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
devices = append(devices, gidRes.DeviceInfo...) |
||||
|
} |
||||
|
|
||||
|
for _, mDevice := range devices { |
||||
|
id := fmt.Sprintf("%s:%d", o.ID, mDevice.DeviceID) |
||||
|
|
||||
|
if o.internalMap[id] == 0 { |
||||
|
o.genMap[id] = subDomainToGeneration(mDevice.SubDomainID) |
||||
|
o.internalMap[id] = mDevice.DeviceID |
||||
|
o.nameMap[id] = mDevice.DeviceName |
||||
|
o.stateMap[id] = device.State{ |
||||
|
Power: gentools.Ptr(mDevice.PowerStatus > 0), |
||||
|
Temperature: gentools.Ptr(float64(mDevice.HolidayTemp)), |
||||
|
} |
||||
|
|
||||
|
// Only register devices if generation is configured
|
||||
|
if o.allowsGeneration(o.genMap[id]) { |
||||
|
o.bus.RunEvent(events.DeviceReady{ID: id}) |
||||
|
o.bus.RunEvent(events.HardwareMetadata{ |
||||
|
ID: id, |
||||
|
Icon: "heater", |
||||
|
}) |
||||
|
o.bus.RunEvent(events.HardwareState{ |
||||
|
ID: id, |
||||
|
InternalName: mDevice.DeviceName, |
||||
|
SupportFlags: device.SFlagPower | device.SFlagTemperature, |
||||
|
State: o.stateMap[id], |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Write to device if generation is configured
|
||||
|
if o.allowsGeneration(o.genMap[id]) { |
||||
|
targetPower := *o.stateMap[id].Power |
||||
|
targetTemp := int(math.Round(*o.stateMap[id].Temperature)) |
||||
|
|
||||
|
currPower := mDevice.PowerStatus > 0 |
||||
|
currTemp := mDevice.HolidayTemp |
||||
|
|
||||
|
changed := false |
||||
|
|
||||
|
if targetPower != currPower { |
||||
|
err := o.setPower(o.genMap[id], mDevice.SubDomainID, mDevice.DeviceID, targetPower) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
changed = true |
||||
|
} |
||||
|
|
||||
|
if targetTemp != currTemp { |
||||
|
err := o.setTemperature(o.genMap[id], mDevice.SubDomainID, mDevice.DeviceID, targetTemp) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
changed = true |
||||
|
} |
||||
|
|
||||
|
if changed { |
||||
|
o.bus.RunEvent(events.HardwareState{ |
||||
|
ID: id, |
||||
|
InternalName: mDevice.DeviceName, |
||||
|
SupportFlags: device.SFlagPower | device.SFlagTemperature, |
||||
|
State: o.stateMap[id], |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) setPower(gen, subDomainID, internalID int, power bool) error { |
||||
|
powerInt := 0 |
||||
|
if power { |
||||
|
powerInt = 1 |
||||
|
} |
||||
|
|
||||
|
if gen == 2 { |
||||
|
powerReq := deviceControlReqBody{ |
||||
|
SubDomain: strconv.Itoa(subDomainID), |
||||
|
DeviceID: internalID, |
||||
|
TestStatus: 1, |
||||
|
Status: powerInt, |
||||
|
} |
||||
|
err := o.command("deviceControl", powerReq, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} else if gen == 3 { |
||||
|
powerReq := deviceControlGen3Body{ |
||||
|
Operation: "SWITCH", |
||||
|
Status: powerInt, |
||||
|
SubDomain: subDomainID, |
||||
|
DeviceId: internalID, |
||||
|
} |
||||
|
err := o.command("deviceControlGen3ForApp", powerReq, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) setTemperature(gen, subDomainID, internalID, temp int) error { |
||||
|
if gen == 2 { |
||||
|
tempReq := changeInfoReqBody{ |
||||
|
DeviceID: internalID, |
||||
|
Value: temp, |
||||
|
TimeZoneNum: "+02:00", |
||||
|
Key: "holidayTemp", |
||||
|
} |
||||
|
err := o.command("changeDeviceInfo", tempReq, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} else if gen == 3 { |
||||
|
tempReq := deviceControlGen3Body{ |
||||
|
Operation: "SINGLE_CONTROL", |
||||
|
Status: 1, |
||||
|
SubDomain: subDomainID, |
||||
|
DeviceId: internalID, |
||||
|
HoldTemp: temp, |
||||
|
} |
||||
|
err := o.command("deviceControlGen3ForApp", tempReq, nil) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) command(command string, payload interface{}, target interface{}) error { |
||||
|
err := o.authenticate() |
||||
|
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(o.token)) |
||||
|
signature := fmt.Sprintf("%x", h.Sum(nil)) |
||||
|
|
||||
|
body, err := json.Marshal(payload) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
req, err := http.NewRequest(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", o.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 fmt.Errorf("command %s failed (%s)", command, err.Error()) |
||||
|
} else if res.StatusCode != 200 { |
||||
|
return fmt.Errorf("command %s failed (negative response)", command) |
||||
|
} |
||||
|
|
||||
|
if target == nil { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
err = json.NewDecoder(res.Body).Decode(&target) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("command %s failed (decoding response)", command) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) authenticate() error { |
||||
|
o.mx.Lock() |
||||
|
defer o.mx.Unlock() |
||||
|
|
||||
|
if o.mustRefreshBy.Before(time.Now().Add(-1 * time.Minute)) { |
||||
|
body, err := json.Marshal(authReqBody{ |
||||
|
Account: o.Email, |
||||
|
Password: o.Password, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
return errors.New("failed to authenticate (marshaling request body)") |
||||
|
} |
||||
|
|
||||
|
req, err := http.NewRequest("POST", accountEndpoint+"login", bytes.NewReader(body)) |
||||
|
if err != nil { |
||||
|
return errors.New("failed to authenticate (creating request)") |
||||
|
} |
||||
|
|
||||
|
addDefaultHeaders(req) |
||||
|
|
||||
|
res, err := http.DefaultClient.Do(req) |
||||
|
if err != nil { |
||||
|
return errors.New("failed to authenticate (internal error)") |
||||
|
} else if res.StatusCode != 200 { |
||||
|
return errors.New("failed to authenticate (negative result)") |
||||
|
} |
||||
|
|
||||
|
var resBody authResBody |
||||
|
err = json.NewDecoder(res.Body).Decode(&resBody) |
||||
|
if err != nil { |
||||
|
return errors.New("failed to authenticate (parsing response body)") |
||||
|
} |
||||
|
|
||||
|
log.Printf("Mill: Authenticated as %s", resBody.NickName) |
||||
|
o.userId = resBody.UserID |
||||
|
o.token = resBody.Token |
||||
|
o.mustRefreshBy, err = time.ParseInLocation("2006-01-02 15:04:05", resBody.TokenExpire, location) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (o *OnlineBridge) allowsGeneration(gen int) bool { |
||||
|
if gen == 2 { |
||||
|
return o.Gen2 |
||||
|
} |
||||
|
|
||||
|
if gen == 3 { |
||||
|
return o.Gen3 |
||||
|
} |
||||
|
|
||||
|
return false |
||||
|
} |
@ -0,0 +1,120 @@ |
|||||
|
package mill |
||||
|
|
||||
|
import ( |
||||
|
"crypto/rand" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
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") |
||||
|
} |
||||
|
|
||||
|
var gen2subDomains = []int{863, 5316, 5317, 5332, 5333, 6933} |
||||
|
|
||||
|
func subDomainToGeneration(subDomain int) int { |
||||
|
for _, gen2sd := range gen2subDomains { |
||||
|
if subDomain == gen2sd { |
||||
|
return 2 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return 3 |
||||
|
} |
||||
|
|
||||
|
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"` |
||||
|
} |
||||
|
|
||||
|
type deviceControlGen3Body struct { |
||||
|
Operation string `json:"operation"` |
||||
|
Status int `json:"status"` |
||||
|
SubDomain int `json:"subDomain"` |
||||
|
DeviceId int `json:"deviceId"` |
||||
|
HoldTemp int `json:"holdTemp,omitempty"` |
||||
|
} |
||||
|
|
||||
|
var location *time.Location |
||||
|
|
||||
|
func init() { |
||||
|
myLocation, err := time.LoadLocation("Europe/Oslo") |
||||
|
if err != nil { |
||||
|
panic(err.Error()) |
||||
|
} |
||||
|
|
||||
|
location = myLocation |
||||
|
} |
@ -0,0 +1,145 @@ |
|||||
|
package mill |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
lucifer3 "git.aiterp.net/lucifer3/server" |
||||
|
"git.aiterp.net/lucifer3/server/commands" |
||||
|
"git.aiterp.net/lucifer3/server/device" |
||||
|
"git.aiterp.net/lucifer3/server/events" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
func NewService() lucifer3.ActiveService { |
||||
|
return &service{ |
||||
|
bridges: make([]Bridge, 0, 8), |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
type service struct { |
||||
|
mu sync.Mutex |
||||
|
|
||||
|
bridges []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 cmd := command.(type) { |
||||
|
case commands.PairDevice: |
||||
|
// Only mill
|
||||
|
if _, ok := cmd.Matches("mill"); !ok { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Run connection if needed
|
||||
|
err := s.connect(bus, cmd.ID, &cmd.APIKey, nil) |
||||
|
if err != nil { |
||||
|
bus.RunEvent(events.DeviceFailed{ |
||||
|
ID: cmd.ID, |
||||
|
Error: err.Error(), |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Connection successful
|
||||
|
bus.RunEvent(events.DeviceAccepted{ |
||||
|
ID: cmd.ID, |
||||
|
APIKey: cmd.APIKey, |
||||
|
}) |
||||
|
case commands.ConnectDevice: |
||||
|
// Connect if necessary
|
||||
|
err := s.connect(bus, cmd.ID, &cmd.APIKey, nil) |
||||
|
if err != nil { |
||||
|
bus.RunEvent(events.DeviceFailed{ |
||||
|
ID: cmd.ID, |
||||
|
Error: err.Error(), |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
case commands.SetState: |
||||
|
err := s.connect(bus, cmd.ID, nil, &cmd.State) |
||||
|
if err != nil { |
||||
|
bus.RunEvent(events.DeviceFailed{ |
||||
|
ID: cmd.ID, |
||||
|
Error: err.Error(), |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
case commands.SetStateBatch: |
||||
|
for id, state := range cmd { |
||||
|
err := s.connect(bus, id, nil, &state) |
||||
|
if err != nil { |
||||
|
bus.RunEvent(events.DeviceFailed{ |
||||
|
ID: id, |
||||
|
Error: err.Error(), |
||||
|
}) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (s *service) connect( |
||||
|
bus *lucifer3.EventBus, |
||||
|
id string, |
||||
|
apiKey *string, |
||||
|
state *device.State, |
||||
|
) error { |
||||
|
hasState := state != nil |
||||
|
if !hasState { |
||||
|
state = &device.State{} |
||||
|
} |
||||
|
|
||||
|
// Check if connection is active
|
||||
|
for _, bridge := range s.bridges { |
||||
|
// Don't block with dead bridges
|
||||
|
if !bridge.IsStarted() { |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Setting a dummy state to the bridge to check if it's online
|
||||
|
bridge.SetBus(bus) |
||||
|
if ok := bridge.SetState(id, *state); ok { |
||||
|
return nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if apiKey == nil { |
||||
|
return errors.New("not connected to device " + id) |
||||
|
} |
||||
|
|
||||
|
// Make a bridge, if not a mill ID, this will return ok false
|
||||
|
bridge, ok := MakeBridge(id, *apiKey) |
||||
|
if !ok { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
bridge.SetBus(bus) |
||||
|
|
||||
|
// Start bridge, and throw error if it failed to start
|
||||
|
bridge.Start() |
||||
|
if !bridge.IsStarted() { |
||||
|
return errors.New("failed to connect to " + id) |
||||
|
} |
||||
|
|
||||
|
// Add bridge to lists
|
||||
|
s.bridges = append(s.bridges, bridge) |
||||
|
|
||||
|
// Update state, if needed, after first connection
|
||||
|
if hasState { |
||||
|
if ok := bridge.SetState(id, *state); !ok { |
||||
|
return errors.New("unable to set state to " + id + " after connecting") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,39 @@ |
|||||
|
package mill |
||||
|
|
||||
|
import ( |
||||
|
lucifer3 "git.aiterp.net/lucifer3/server" |
||||
|
"git.aiterp.net/lucifer3/server/device" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
) |
||||
|
|
||||
|
type WifiBridge struct { |
||||
|
ID string |
||||
|
IP string |
||||
|
|
||||
|
mx sync.Mutex |
||||
|
started bool |
||||
|
bus *lucifer3.EventBus |
||||
|
} |
||||
|
|
||||
|
func (w *WifiBridge) SetBus(bus *lucifer3.EventBus) { |
||||
|
w.bus = bus |
||||
|
} |
||||
|
|
||||
|
func (w *WifiBridge) SetState(id string, state device.State) bool { |
||||
|
if !strings.HasPrefix(id, "mill:3:"+w.IP) { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
func (w *WifiBridge) Start() { |
||||
|
go func() { |
||||
|
|
||||
|
}() |
||||
|
} |
||||
|
|
||||
|
func (w *WifiBridge) IsStarted() bool { |
||||
|
return w.started |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue