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.
321 lines
7.3 KiB
321 lines
7.3 KiB
package mill
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"git.aiterp.net/lucifer/new-server/internal/lerrors"
|
|
"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 {
|
|
if ip, ok := deviceModel.DriverProperties["ip"].(string); ok {
|
|
return b.pushWifiStateChange(ctx, ip, deviceModel.State.Temperature)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
if b.luciferMillIDMap == nil {
|
|
b.luciferMillIDMap = make(map[int]int, 4)
|
|
b.millLuciferIDMap = make(map[int]int, 4)
|
|
}
|
|
subDomain := deviceModel.DriverProperties["subDomain"].(string)
|
|
|
|
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
|
|
}
|
|
|
|
if subDomainIsGen2(subDomain) {
|
|
powerReq := deviceControlReqBody{
|
|
SubDomain: subDomain,
|
|
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
|
|
}
|
|
} else {
|
|
sd, _ := strconv.Atoi(subDomain)
|
|
|
|
tempReq := deviceControlGen3Body{
|
|
Operation: "SINGLE_CONTROL",
|
|
Status: status,
|
|
SubDomain: sd,
|
|
DeviceId: b.luciferMillIDMap[deviceModel.ID],
|
|
HoldTemp: deviceModel.State.Temperature,
|
|
}
|
|
err := b.command(ctx, "deviceControlGen3ForApp", tempReq, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
powerReq := deviceControlGen3Body{
|
|
Operation: "SWITCH",
|
|
Status: status,
|
|
SubDomain: sd,
|
|
DeviceId: b.luciferMillIDMap[deviceModel.ID],
|
|
}
|
|
err = b.command(ctx, "deviceControlGen3ForApp", powerReq, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *bridge) pushWifiStateChange(ctx context.Context, ip string, temperature int) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
data, err := json.Marshal(wifiSetTemperatureBody{
|
|
Type: "Normal",
|
|
Value: temperature,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(
|
|
ctx,
|
|
"POST",
|
|
fmt.Sprintf("http://%s/set-temperature", ip),
|
|
bytes.NewReader(data),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if res.StatusCode > 299 {
|
|
return errors.New("mill: negative response from " + ip)
|
|
}
|
|
|
|
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 lerrors.ErrCannotForwardRequest
|
|
} else if res.StatusCode != 200 {
|
|
return lerrors.ErrIncorrectToken
|
|
}
|
|
|
|
if target == nil {
|
|
return nil
|
|
}
|
|
|
|
err = json.NewDecoder(res.Body).Decode(&target)
|
|
if err != nil {
|
|
return lerrors.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 lerrors.ErrMissingToken
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", accountEndpoint+"login", bytes.NewReader(body))
|
|
if err != nil {
|
|
return lerrors.ErrMissingToken
|
|
}
|
|
|
|
addDefaultHeaders(req)
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return lerrors.ErrCannotForwardRequest
|
|
} else if res.StatusCode != 200 {
|
|
return lerrors.ErrIncorrectToken
|
|
}
|
|
|
|
var resBody authResBody
|
|
err = json.NewDecoder(res.Body).Decode(&resBody)
|
|
if err != nil {
|
|
return lerrors.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")
|
|
}
|
|
|
|
var gen2subDomains = []string{"863", "5316", "5317", "5332", "5333", "6933"}
|
|
|
|
func subDomainIsGen2(subDomain string) bool {
|
|
for _, gen2sd := range gen2subDomains {
|
|
if subDomain == gen2sd {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|