package mill import ( "bytes" "context" "crypto/rand" "crypto/sha1" "encoding/json" "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 { b.mu.Lock() if b.luciferMillIDMap == nil { b.luciferMillIDMap = make(map[int]int, 4) b.millLuciferIDMap = make(map[int]int, 4) } 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 } powerReq := deviceControlReqBody{ SubDomain: deviceModel.DriverProperties["subDomain"].(string), 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 } 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") }