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.
367 lines
8.0 KiB
367 lines
8.0 KiB
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() {
|
|
recover()
|
|
|
|
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
|
|
}
|