482 lines
10 KiB
Go
482 lines
10 KiB
Go
// backend\milestone_child_devices.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
type milestoneNamedDevice struct {
|
|
Collection string
|
|
ID string
|
|
Name string
|
|
DisplayName string
|
|
ShortName string
|
|
Enabled bool
|
|
RecordingEnabled bool
|
|
RecordingStorageID string
|
|
}
|
|
|
|
func replaceMilestoneDeviceBaseName(
|
|
deviceName string,
|
|
oldHardwareName string,
|
|
newHardwareName string,
|
|
) (string, bool) {
|
|
deviceName = strings.TrimSpace(deviceName)
|
|
oldHardwareName = strings.TrimSpace(oldHardwareName)
|
|
newHardwareName = strings.TrimSpace(newHardwareName)
|
|
|
|
if deviceName == "" || newHardwareName == "" {
|
|
return deviceName, false
|
|
}
|
|
|
|
if oldHardwareName != "" {
|
|
if deviceName == oldHardwareName {
|
|
return newHardwareName, true
|
|
}
|
|
|
|
if strings.HasPrefix(deviceName, oldHardwareName) {
|
|
suffix := strings.TrimPrefix(deviceName, oldHardwareName)
|
|
|
|
if suffix == "" ||
|
|
strings.HasPrefix(suffix, " ") ||
|
|
strings.HasPrefix(suffix, "-") ||
|
|
strings.HasPrefix(suffix, "_") {
|
|
return newHardwareName + suffix, true
|
|
}
|
|
}
|
|
}
|
|
|
|
if baseName, suffix, found := strings.Cut(deviceName, " - "); found {
|
|
baseName = strings.TrimSpace(baseName)
|
|
suffix = strings.TrimSpace(suffix)
|
|
|
|
if baseName != "" && suffix != "" && baseName != newHardwareName {
|
|
return newHardwareName + " - " + suffix, true
|
|
}
|
|
}
|
|
|
|
return deviceName, false
|
|
}
|
|
|
|
func milestoneItemEnabledValue(item map[string]any) bool {
|
|
enabled := true
|
|
|
|
if value, ok := item["enabled"].(bool); ok {
|
|
enabled = value
|
|
}
|
|
|
|
if value, ok := item["disabled"].(bool); ok {
|
|
enabled = !value
|
|
}
|
|
|
|
return enabled
|
|
}
|
|
|
|
func milestoneNamedDevicesFromHardware(hardware map[string]any) []milestoneNamedDevice {
|
|
devices := []milestoneNamedDevice{}
|
|
|
|
devices = append(devices, milestoneNamedDevicesFromHardwareCollection(hardware, "cameras")...)
|
|
devices = append(devices, milestoneNamedDevicesFromHardwareCollection(hardware, "microphones")...)
|
|
|
|
return devices
|
|
}
|
|
|
|
func milestoneNamedDevicesFromHardwareCollectionOnly(
|
|
hardware map[string]any,
|
|
collection string,
|
|
) []milestoneNamedDevice {
|
|
return milestoneNamedDevicesFromHardwareCollection(hardware, collection)
|
|
}
|
|
|
|
func milestoneNamedDevicesFromHardwareCollection(
|
|
hardware map[string]any,
|
|
collection string,
|
|
) []milestoneNamedDevice {
|
|
rawItems, ok := hardware[collection].([]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
devices := make([]milestoneNamedDevice, 0, len(rawItems))
|
|
|
|
for _, rawItem := range rawItems {
|
|
item, ok := rawItem.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
id := milestoneNamedDeviceID(item)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
|
|
name := milestoneStringValue(item["name"])
|
|
displayName := milestoneStringValue(item["displayName"])
|
|
shortName := milestoneStringValue(item["shortName"])
|
|
|
|
if name == "" {
|
|
name = displayName
|
|
}
|
|
if name == "" {
|
|
name = shortName
|
|
}
|
|
if displayName == "" {
|
|
displayName = name
|
|
}
|
|
if shortName == "" {
|
|
shortName = name
|
|
}
|
|
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
enabled := milestoneItemEnabledValue(item)
|
|
|
|
recordingEnabled := false
|
|
if value, ok := item["recordingEnabled"].(bool); ok {
|
|
recordingEnabled = value
|
|
}
|
|
|
|
recordingStorageID := ""
|
|
if recordingStorage, ok := item["recordingStorage"].(map[string]any); ok {
|
|
recordingStorageID = strings.TrimSpace(milestoneStringValue(recordingStorage["id"]))
|
|
}
|
|
|
|
devices = append(devices, milestoneNamedDevice{
|
|
Collection: collection,
|
|
ID: id,
|
|
Name: name,
|
|
DisplayName: displayName,
|
|
ShortName: shortName,
|
|
Enabled: enabled,
|
|
RecordingEnabled: recordingEnabled,
|
|
RecordingStorageID: recordingStorageID,
|
|
})
|
|
}
|
|
|
|
return devices
|
|
}
|
|
|
|
func mergeMilestoneNamedDevices(groups ...[]milestoneNamedDevice) []milestoneNamedDevice {
|
|
result := []milestoneNamedDevice{}
|
|
seen := map[string]bool{}
|
|
|
|
for _, group := range groups {
|
|
for _, device := range group {
|
|
key := device.Collection + ":" + device.ID
|
|
if device.ID == "" || seen[key] {
|
|
continue
|
|
}
|
|
|
|
seen[key] = true
|
|
result = append(result, device)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func requestMilestoneNamedDevicesByHardware(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
collection string,
|
|
hardwareID string,
|
|
) ([]milestoneNamedDevice, error) {
|
|
switch collection {
|
|
case "cameras", "microphones":
|
|
default:
|
|
return nil, fmt.Errorf("unsupported milestone device collection: %s", collection)
|
|
}
|
|
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/" +
|
|
collection +
|
|
"?disabled"
|
|
|
|
request, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodGet,
|
|
requestURL,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tokenType := strings.TrimSpace(config.TokenType)
|
|
if tokenType == "" {
|
|
tokenType = "Bearer"
|
|
}
|
|
|
|
request.Header.Set("Authorization", tokenType+" "+config.AccessToken)
|
|
request.Header.Set("Accept", "application/json")
|
|
|
|
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, _ := io.ReadAll(response.Body)
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, newAppErrorf(
|
|
"milestone child device list failed",
|
|
LogFields{
|
|
"statusCode": response.StatusCode,
|
|
"responseBody": string(responseBody),
|
|
"collection": collection,
|
|
"hardwareId": hardwareID,
|
|
},
|
|
"milestone child device list failed: collection=%s hardwareId=%s status=%d body=%s",
|
|
collection,
|
|
hardwareID,
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return milestoneNamedDevicesFromListResponse(responseBody, collection, hardwareID)
|
|
}
|
|
|
|
func milestoneNamedDevicesFromListResponse(
|
|
body []byte,
|
|
collection string,
|
|
hardwareID string,
|
|
) ([]milestoneNamedDevice, error) {
|
|
var decoded map[string]any
|
|
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rawItems []any
|
|
|
|
if array, ok := decoded["array"].([]any); ok {
|
|
rawItems = array
|
|
} else if array, ok := decoded["data"].([]any); ok {
|
|
rawItems = array
|
|
} else if data, ok := decoded["data"].(map[string]any); ok {
|
|
if array, ok := data["array"].([]any); ok {
|
|
rawItems = array
|
|
}
|
|
}
|
|
|
|
if len(rawItems) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
devices := make([]milestoneNamedDevice, 0, len(rawItems))
|
|
hardwareID = strings.TrimSpace(hardwareID)
|
|
|
|
for _, rawItem := range rawItems {
|
|
item, ok := rawItem.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
parentID := milestoneNamedDeviceParentID(item)
|
|
if parentID != hardwareID {
|
|
continue
|
|
}
|
|
|
|
id := milestoneNamedDeviceID(item)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
|
|
displayName := milestoneStringValue(item["displayName"])
|
|
shortName := milestoneStringValue(item["shortName"])
|
|
name := milestoneStringValue(item["name"])
|
|
|
|
if name == "" {
|
|
name = displayName
|
|
}
|
|
if name == "" {
|
|
name = shortName
|
|
}
|
|
if displayName == "" {
|
|
displayName = name
|
|
}
|
|
if shortName == "" {
|
|
shortName = name
|
|
}
|
|
if name == "" {
|
|
continue
|
|
}
|
|
|
|
enabled := milestoneItemEnabledValue(item)
|
|
|
|
recordingEnabled := false
|
|
if value, ok := item["recordingEnabled"].(bool); ok {
|
|
recordingEnabled = value
|
|
}
|
|
|
|
recordingStorageID := ""
|
|
if recordingStorage, ok := item["recordingStorage"].(map[string]any); ok {
|
|
recordingStorageID = strings.TrimSpace(milestoneStringValue(recordingStorage["id"]))
|
|
}
|
|
|
|
devices = append(devices, milestoneNamedDevice{
|
|
Collection: collection,
|
|
ID: id,
|
|
Name: name,
|
|
DisplayName: displayName,
|
|
ShortName: shortName,
|
|
Enabled: enabled,
|
|
RecordingEnabled: recordingEnabled,
|
|
RecordingStorageID: recordingStorageID,
|
|
})
|
|
}
|
|
|
|
return devices, nil
|
|
}
|
|
|
|
func milestoneNamedDeviceParentID(item map[string]any) string {
|
|
relations, ok := item["relations"].(map[string]any)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
parent, ok := relations["parent"].(map[string]any)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
return milestoneStringValue(parent["id"])
|
|
}
|
|
|
|
func milestoneNamedDeviceID(item map[string]any) string {
|
|
if id := milestoneStringValue(item["id"]); id != "" {
|
|
return id
|
|
}
|
|
|
|
relations, ok := item["relations"].(map[string]any)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
self, ok := relations["self"].(map[string]any)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
return milestoneStringValue(self["id"])
|
|
}
|
|
|
|
func updateMilestoneChildDeviceBaseNames(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
devices []milestoneNamedDevice,
|
|
oldHardwareName string,
|
|
newHardwareName string,
|
|
) error {
|
|
for _, device := range devices {
|
|
nextName, changed := replaceMilestoneDeviceBaseName(
|
|
device.Name,
|
|
oldHardwareName,
|
|
newHardwareName,
|
|
)
|
|
if !changed {
|
|
continue
|
|
}
|
|
|
|
if err := patchMilestoneNamedDevice(
|
|
ctx,
|
|
config,
|
|
device.Collection,
|
|
device.ID,
|
|
map[string]any{
|
|
"name": nextName,
|
|
"displayName": nextName,
|
|
"shortName": nextName,
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func patchMilestoneNamedDevice(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
collection string,
|
|
deviceID string,
|
|
payload map[string]any,
|
|
) error {
|
|
switch collection {
|
|
case "cameras", "microphones":
|
|
default:
|
|
return fmt.Errorf("unsupported milestone device collection: %s", collection)
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/" +
|
|
collection +
|
|
"/" +
|
|
url.PathEscape(deviceID)
|
|
|
|
request, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodPatch,
|
|
requestURL,
|
|
bytes.NewReader(body),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tokenType := strings.TrimSpace(config.TokenType)
|
|
if tokenType == "" {
|
|
tokenType = "Bearer"
|
|
}
|
|
|
|
request.Header.Set("Authorization", tokenType+" "+config.AccessToken)
|
|
request.Header.Set("Accept", "application/json")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, _ := io.ReadAll(response.Body)
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return newAppErrorf(
|
|
"milestone child device patch failed",
|
|
LogFields{
|
|
"statusCode": response.StatusCode,
|
|
"responseBody": string(responseBody),
|
|
"collection": collection,
|
|
"deviceId": deviceID,
|
|
"payload": payload,
|
|
},
|
|
"milestone child device patch failed: collection=%s id=%s status=%d body=%s",
|
|
collection,
|
|
deviceID,
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|