From 9c3b339ceff93ff7d8be63d6e055629d7b724faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20Hilligs=C3=B8e?= Date: Sun, 23 Apr 2023 19:02:06 +0000 Subject: [PATCH] Lots --- examples/get_info/get_info.go | 14 +++- kefw2/eqprofilev2.go | 25 +++++++ kefw2/http.go | 134 ++++++++++++++++++++++++++++++++-- kefw2/json-parsing.go | 44 ----------- kefw2/json_parsing.go | 78 ++++++++++++++++++++ kefw2/kefw2.go | 58 +++++++-------- kefw2/source.go | 20 +++++ kefw2/speaker_status.go | 15 ++++ research/commands.http | 44 ++++++++++- 9 files changed, 342 insertions(+), 90 deletions(-) create mode 100644 kefw2/eqprofilev2.go delete mode 100644 kefw2/json-parsing.go create mode 100644 kefw2/json_parsing.go create mode 100644 kefw2/source.go create mode 100644 kefw2/speaker_status.go diff --git a/examples/get_info/get_info.go b/examples/get_info/get_info.go index e799617..b1114a2 100644 --- a/examples/get_info/get_info.go +++ b/examples/get_info/get_info.go @@ -19,8 +19,18 @@ func main() { fmt.Println("MAC Address:", speaker.MacAddress) volume, _ := speaker.GetVolume() fmt.Println("Volume:", volume) - source, _ := speaker.GetSource() + source, _ := speaker.Source() fmt.Println("Source:", source) - powerstate, _ := speaker.GetPowerState() + powerstate, _ := speaker.IsPoweredOn() fmt.Println("Powered on:", powerstate) + // speaker.PowerOff() + // err = speaker.Unmute() + // if err != nil { + // log.Fatal(err) + // } + speaker.PlayPause() + err = speaker.SetSource(kefw2.SourceTV) + if err != nil { + fmt.Println(err) + } } diff --git a/kefw2/eqprofilev2.go b/kefw2/eqprofilev2.go new file mode 100644 index 0000000..6fa48c0 --- /dev/null +++ b/kefw2/eqprofilev2.go @@ -0,0 +1,25 @@ +package kefw2 + +type EQProfileV2 struct { + SubwooferCount int `json:"subwooferCount"` // 0, 1, 2 + TrebleAmount float32 `json:"trebleAmount"` + DeskMode bool `json:"deskMode"` + BassExtension string `json:"bassExtension"` // less, standard, more + HighPassMode bool `json:"highPassMode"` + AudioPolarity string `json:"audioPolarity"` + IsExpertMode bool `json:"isExpertMode"` + DeskModeSetting int `json:"deskModeSetting"` + SubwooferPreset string `json:"subwooferPreset"` + HighPassModeFreq int `json:"highPassModeFreq"` + WallModeSetting float32 `json:"wallModeSetting"` + Balance int `json:"balance"` + SubEnableStereo bool `json:"subEnableStereo"` + SubwooferPolarity string `json:"subwooferPolarity"` + SubwooferGain int `json:"subwooferGain"` + IsKW1 bool `json:"isKW1"` + PhaseCorrection bool `json:"phaseCorrection"` + WallMode bool `json:"wallMode"` + ProfileId string `json:"profileId"` + ProfileName string `json:"profileName"` + SubOutLPFreq float32 `json:"subOutLPFreq"` +} diff --git a/kefw2/http.go b/kefw2/http.go index 650ddfb..e103ecf 100644 --- a/kefw2/http.go +++ b/kefw2/http.go @@ -1,6 +1,8 @@ package kefw2 import ( + "bytes" + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -8,46 +10,57 @@ import ( log "github.com/sirupsen/logrus" ) +type KEFPostRequest struct { + Path string `json:"path"` + Roles string `json:"roles"` + Value *json.RawMessage `json:"value"` +} + func (s KEFSpeaker) getData(path string) ([]byte, error) { + // log.SetLevel(log.DebugLevel) client := &http.Client{} req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/api/getData", s.IPAddress), nil) if err != nil { return nil, err } + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + q := req.URL.Query() q.Add("path", path) q.Add("roles", "value") req.URL.RawQuery = q.Encode() - log.Debug("Request:", req.URL.String()) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() - if resp.StatusCode != 200 { - log.Debug("Response:", resp.StatusCode, resp.Body) - return nil, fmt.Errorf("HTTP Status Code: %d\n%s", resp.StatusCode, resp.Body) - } - body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } + if resp.StatusCode != 200 { + log.Debug("Response:", resp.StatusCode, resp.Body) + return nil, fmt.Errorf("HTTP Status Code: %d\n%s", resp.StatusCode, resp.Body) + } + return body, nil } func (s KEFSpeaker) getRows(path string, params map[string]string) ([]byte, error) { - // log.SetLevel(log.DebugLevel) client := &http.Client{} req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/api/getRows", s.IPAddress), nil) if err != nil { return nil, err } + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + q := req.URL.Query() q.Add("path", path) // Always add the path for key, value := range params { @@ -55,7 +68,6 @@ func (s KEFSpeaker) getRows(path string, params map[string]string) ([]byte, erro } req.URL.RawQuery = q.Encode() - log.Debug("Request:", req.URL.String()) resp, err := client.Do(req) if err != nil { return nil, err @@ -74,3 +86,109 @@ func (s KEFSpeaker) getRows(path string, params map[string]string) ([]byte, erro return body, nil } + +func (s KEFSpeaker) setActivate(path, item, value string) error { + client := &http.Client{} + + jsonStr, _ := json.Marshal( + map[string]string{ + item: value, + }) + rawValue := json.RawMessage(jsonStr) + + reqbody, _ := json.Marshal(KEFPostRequest{ + Path: path, + Roles: "activate", + Value: &rawValue, + }) + + req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/setData", s.IPAddress), bytes.NewBuffer(reqbody)) + if err != nil { + return err + } + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Debug("Response:", resp.StatusCode, resp.Body) + return fmt.Errorf("HTTP Status Code: %d\n%s", resp.StatusCode, resp.Body) + } + + // body, err := ioutil.ReadAll(resp.Body) + // if err != nil { + // return nil, err + // } + + return nil +} + +func (s KEFSpeaker) setTypedValue(path string, value any) error { + client := &http.Client{} + + var myType string + var myValue string + switch theType := value.(type) { + case int: + myType = "i32_" + myValue = fmt.Sprintf("%d", value.(int)) + case string: + myType = "string_" + myValue = fmt.Sprintf("\"%s\"", value.(string)) + case bool: + myType = "bool_" + myValue = fmt.Sprintf("%t", value.(bool)) + case Source: + myType = "kefPhysicalSource" + myValue = fmt.Sprintf("\"%s\"", value.(Source)) + case SpeakerStatus: + myType = "kefSpeakerStatus" + myValue = fmt.Sprintf("\"%s\"", value.(SpeakerStatus)) + default: + return fmt.Errorf("type %s is not supported", theType) + } + + // Build the JSON + jsonStr, _ := json.Marshal( + map[string]string{ + "type": myType, + myType: myValue, + }) + rawValue := json.RawMessage(jsonStr) + pr := KEFPostRequest{ + Path: path, + Roles: "value", + Value: &rawValue, + } + + reqbody, _ := json.MarshalIndent(pr, "", " ") + req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/setData", s.IPAddress), bytes.NewBuffer(reqbody)) + if err != nil { + return err + } + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + log.Debug("Response:", resp.StatusCode, resp.Body) + return fmt.Errorf("HTTP Status Code: %d\n%s", resp.StatusCode, resp.Body) + } + + // body, err := ioutil.ReadAll(resp.Body) + // if err != nil { + // return nil, err + // } + + return nil +} diff --git a/kefw2/json-parsing.go b/kefw2/json-parsing.go deleted file mode 100644 index 6538903..0000000 --- a/kefw2/json-parsing.go +++ /dev/null @@ -1,44 +0,0 @@ -package kefw2 - -import ( - "encoding/json" -) - -func JSONStringValue(data []byte, err error) (value string, err2 error) { - if err != nil { - return "", err - } - var jsonData []map[string]interface{} - err2 = json.Unmarshal(data, &jsonData) - if err2 != nil { - return "", err - } - value = jsonData[0]["string_"].(string) - return value, nil -} - -func JSONStringValueByKey(data []byte, key string, err error) (value string, err2 error) { - if err != nil { - return "", err - } - var jsonData []map[string]interface{} - err2 = json.Unmarshal(data, &jsonData) - if err2 != nil { - return "", err - } - value = jsonData[0][key].(string) - return value, nil -} - -func JSONIntValue(data []byte, err error) (value int, err2 error) { - if err != nil { - return 0, err - } - var jsonData []map[string]interface{} - err2 = json.Unmarshal(data, &jsonData) - if err2 != nil { - return 0, err - } - fvalue, _ := jsonData[0]["i32_"].(float64) - return int(fvalue), nil -} diff --git a/kefw2/json_parsing.go b/kefw2/json_parsing.go new file mode 100644 index 0000000..7524419 --- /dev/null +++ b/kefw2/json_parsing.go @@ -0,0 +1,78 @@ +package kefw2 + +import ( + "encoding/json" + "errors" +) + +func JSONStringValue(data []byte, err error) (value string, err2 error) { + if err != nil { + return "", err + } + var jsonData []map[string]interface{} + err2 = json.Unmarshal(data, &jsonData) + if err2 != nil { + return "", err + } + value = jsonData[0]["string_"].(string) + return value, nil +} + +func JSONStringValueByKey(data []byte, key string, err error) (value string, err2 error) { + if err != nil { + return "", err + } + var jsonData []map[string]string + err2 = json.Unmarshal(data, &jsonData) + if err2 != nil { + return "", err + } + value = jsonData[0]["value"] + return value, nil +} + +func JSONIntValue(data []byte, err error) (value int, err2 error) { + if err != nil { + return 0, err + } + var jsonData []map[string]interface{} + err2 = json.Unmarshal(data, &jsonData) + if err2 != nil { + return 0, err + } + fvalue, _ := jsonData[0]["i32_"].(float64) + return int(fvalue), nil +} + +func JSONUnmarshalValue(data []byte, err error) (value any, err2 error) { + // Easing the call chain + if err != nil { + return 0, err + } + + // Unmarshal the JSON data into a map of strings to any + var jsonData []map[string]any + err2 = json.Unmarshal(data, &jsonData) + if err2 != nil { + return 0, err + } + // Locate the value and set the type + tvalue := jsonData[0]["type"].(string) + switch tvalue { + case "i32_": + value = jsonData[0]["i32_"].(int) + case "i64_": + value = jsonData[0]["i64_"].(int) + case "string_": + value = jsonData[0]["string_"].(string) + case "bool_": + value = jsonData[0]["bool_"].(bool) + case "kefPhysicalSource": + value = Source(jsonData[0]["kefPhysicalSource"].(string)) + case "kefSpeakerStatus": + value = SpeakerStatus(jsonData[0]["kefSpeakerStatus"].(string)) + default: + return nil, errors.New("Unknown type: " + tvalue) + } + return value, nil +} diff --git a/kefw2/kefw2.go b/kefw2/kefw2.go index ab54b73..393439c 100644 --- a/kefw2/kefw2.go +++ b/kefw2/kefw2.go @@ -14,18 +14,6 @@ type KEFSpeaker struct { Id string } -type Source string - -const ( - SourceStandby Source = "standby" - SourceOptical Source = "optical" - SourceCoaxial Source = "coaxial" - SourceBluetooth Source = "bluetooth" - SourceAux Source = "aux" - SourceUsb Source = "usb" - SourceWifi Source = "wifi" -) - var ( Models = map[string]string{ "lsx2": "KEF LSX II", @@ -99,47 +87,53 @@ func (s *KEFSpeaker) getModelAndId() (err error) { return err } +func (s KEFSpeaker) PlayPause() error { + return s.setActivate("player:player/control", "control", "pause") +} + func (s KEFSpeaker) GetVolume() (volume int, err error) { return JSONIntValue(s.getData("player:volume")) } func (s KEFSpeaker) SetVolume(volume int) error { - return nil + path := "player:volume" + return s.setTypedValue(path, volume) } func (s KEFSpeaker) Mute() error { - return nil + path := "settings:/mediaPlayer/mute" + return s.setTypedValue(path, true) } func (s KEFSpeaker) Unmute() error { - return nil + path := "settings:/mediaPlayer/mute" + return s.setTypedValue(path, false) } -func (s KEFSpeaker) PowerOn(power bool) error { - return nil -} - -func (s KEFSpeaker) PowerOff(power bool) error { - return nil +// PowerOff set the speaker to standby mode +func (s KEFSpeaker) PowerOff() error { + return s.SetSource(SourceStandby) } func (s KEFSpeaker) SetSource(source Source) error { - fmt.Println("Source to be set:", source) - return nil + path := "settings:/kef/play/physicalSource" + return s.setTypedValue(path, source) } -func (s *KEFSpeaker) GetSource() (source Source, err error) { - var src string - data, err := s.getData("settings:/kef/play/physicalSource") - src, err = JSONStringValueByKey(data, "kefPhysicalSource", err) - return Source(src), err +func (s *KEFSpeaker) Source() (source Source, err error) { + src, err2 := JSONUnmarshalValue(s.getData("settings:/kef/play/physicalSource")) + return src.(Source), err2 } -func (s *KEFSpeaker) GetPowerState() (bool, error) { - data, err := s.getData("settings:/kef/host/speakerStatus") - powerState, err := JSONStringValueByKey(data, "kefSpeakerStatus", err) - if powerState == "powerOn" { +func (s *KEFSpeaker) IsPoweredOn() (bool, error) { + powerState, err := JSONUnmarshalValue(s.getData("settings:/kef/host/speakerStatus")) + if powerState == SpeakerStatusOn { return true, err } return false, err } + +func (s *KEFSpeaker) SpeakerState() (SpeakerStatus, error) { + speakerStatus, err := JSONUnmarshalValue(s.getData("settings:/kef/host/speakerStatus")) + return SpeakerStatus(speakerStatus.(SpeakerStatus)), err +} diff --git a/kefw2/source.go b/kefw2/source.go new file mode 100644 index 0000000..334df9a --- /dev/null +++ b/kefw2/source.go @@ -0,0 +1,20 @@ +package kefw2 + +// Source represents the source of the audio signal (kefPhysicalSource) +type Source string + +const ( + SourceAux Source = "analog" + SourceBluetooth Source = "bluetooth" + SourceCoaxial Source = "coaxial" + SourceOptical Source = "optical" + SourceStandby Source = "standby" + SourceTV Source = "tv" + SourceUsb Source = "usb" + SourceWifi Source = "wifi" +) + +// String returns the string representation of the source +func (s *Source) String() string { + return string(*s) +} diff --git a/kefw2/speaker_status.go b/kefw2/speaker_status.go new file mode 100644 index 0000000..9dc471c --- /dev/null +++ b/kefw2/speaker_status.go @@ -0,0 +1,15 @@ +package kefw2 + +type SpeakerStatus string + +const ( + SpeakerStatusStandby SpeakerStatus = "standby" + SpeakerStatusOn SpeakerStatus = "powerOn" + SpeakerInNetworkSetup SpeakerStatus = "networkSetup" + SpeakerInFirmwareUpgrade SpeakerStatus = "firmwareUpgrade" +) + +// String returns the string representation of the speaker status +func (s *SpeakerStatus) String() string { + return string(*s) +} diff --git a/research/commands.http b/research/commands.http index e69d258..3d2df97 100644 --- a/research/commands.http +++ b/research/commands.http @@ -25,7 +25,23 @@ GET {{baseurl}}/api/event/pollQueue Accept: application/json Content-Type: application/json -### Get MAC Address +### Get poll Queue ID +GET {{baseurl}}/api/event/pollQueue?queueId=%7B35bb7b73-13aa-4e69-8c28-02e26acca869%7D&timeout=5 +Accept: application/json +Content-Type: application/json + +### Get player data +GET {{baseurl}}/api/getData?roles={{roles}}&path=player%3Aplayer%2Fdata +Accept: application/json +Content-Type: application/json + +### Get player data (large object if playing) +GET {{baseurl}}/api/getRows?roles={{roles}}&from=0&to=9&path=airable%3AplayContext%3Ahttps%3A%2F%2F8448239770.airable.io%2Fid%2Ftidal%2Ftrack%2F37944910 +Accept: application/json +Content-Type: application/json + + +### Get Primary MAC Address (Wired) POST {{baseurl}}/api/getData Accept: application/json Content-Type: application/json @@ -35,6 +51,12 @@ Content-Type: application/json "roles": "{{postroles}}" } +### Get Network Info +GET {{baseurl}}/api/getData?path=network%3Ainfo&roles={{roles}} +Accept: application/json +Content-Type: application/json + + ### Get Device Name POST {{baseurl}}/api/getData Accept: application/json @@ -81,7 +103,7 @@ Content-Type: application/json "roles": "value", "value": { "type": "kefPhysicalSource", - "kefPhysicalSource": "wifi" + "kefPhysicalSource": "tv" } } @@ -121,10 +143,24 @@ Content-Type: application/json "roles": "value", "value": { "type": "i32_", - "i32_": 0 + "i32_": 24 + } +} + +### Play/Pause media +POST {{baseurl}}/api/setData +Accept: application/json +Content-Type: application/json + +{ + "path":"player:player\/control", + "role":"activate", + "value": { + "control":"pause" } } + ### Song Progress POST {{baseurl}}/api/getData Accept: application/json @@ -201,7 +237,7 @@ GET {{baseurl}}/api/getData?path=bluetooth%3Astate&roles={{roles}} Accept: application/json Content-Type: application/json -### Get playlists (Returns the number 1000?) +### Get play queue items limit GET {{baseurl}}/api/getData?path=settings%3A%2Fplaylists%2FdbItemsLimit&roles={{roles}} Accept: application/json Content-Type: application/json