|
|
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 }
|