Mill: Online + icon in tradfri
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
lucifer3 "git.aiterp.net/lucifer3/server"
|
||||
"git.aiterp.net/lucifer3/server/services"
|
||||
"git.aiterp.net/lucifer3/server/services/hue"
|
||||
"git.aiterp.net/lucifer3/server/services/mill"
|
||||
"git.aiterp.net/lucifer3/server/services/nanoleaf"
|
||||
"git.aiterp.net/lucifer3/server/services/tradfri"
|
||||
"time"
|
||||
@@ -21,6 +22,7 @@ func main() {
|
||||
bus.Join(nanoleaf.NewService())
|
||||
bus.Join(hue.NewService())
|
||||
bus.Join(tradfri.NewService())
|
||||
bus.Join(mill.NewService())
|
||||
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -107,7 +107,10 @@ func (b *Bridge) listen(bus *lucifer3.EventBus) {
|
||||
b.stateMap[id] = currState
|
||||
|
||||
bus.RunEvent(events.DeviceReady{ID: id})
|
||||
bus.RunEvent(events.HardwareMetadata{ID: id})
|
||||
bus.RunEvent(events.HardwareMetadata{
|
||||
ID: id,
|
||||
Icon: "lightbulb",
|
||||
})
|
||||
bus.RunEvent(events.HardwareState{
|
||||
ID: id,
|
||||
InternalName: ikeaDevice.Name,
|
||||
|
||||
Reference in New Issue
Block a user