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 }