teg/backend/milestone_child_devices.go
2026-06-09 14:26:35 +02:00

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
}