+package protocol
+
+import (
+ "fmt"
+ "strings"
+)
+
+const (
+ // Root is the versioned MQTT namespace.
+ Root = "onehouse/v1"
+
+ // Descriptor identifies retained device metadata.
+ Descriptor Kind = "descriptor"
+ // Availability identifies retained device reachability.
+ Availability Kind = "availability"
+ // Reported identifies a retained point value.
+ Reported Kind = "reported"
+ // Telemetry identifies a non-retained measurement.
+ Telemetry Kind = "telemetry"
+ // Event identifies a non-retained occurrence.
+ Event Kind = "events"
+ // Command identifies a non-retained point request.
+ Command Kind = "commands"
+ // CommandBatch identifies an ordered group of point requests.
+ CommandBatch Kind = "batch"
+ // Result identifies a command outcome.
+ Result Kind = "results"
+ // HubStatus identifies retained Zigbee hub diagnostics.
+ HubStatus Kind = "status"
+)
+
+// Kind identifies a wire operation, independent of its topic text.
+type Kind string
+
+// Address identifies a device operation or a Zigbee hub status. Target is a
+// capability, event, or command ID. Hub status and device metadata omit Target.
+type Address struct {
+ Kind Kind
+ ID string
+ Target string
+}
+
+// Topic validates the address and returns its exact publication topic.
+func (a Address) Topic() (string, error) {
+ if !ValidID(a.ID) {
+ return "", fmt.Errorf("address ID must be a canonical ULID")
+ }
+
+ switch a.Kind {
+ case Descriptor, Availability:
+ if a.Target != "" {
+ return "", fmt.Errorf("metadata address must not have a target")
+ }
+
+ return Root + "/devices/" + a.ID + "/" + string(a.Kind), nil
+ case Reported, Telemetry, Event, Command:
+ if !ValidIdentifier(a.Target) {
+ return "", fmt.Errorf("point address must have a valid target")
+ }
+ case Result:
+ if !ValidID(a.Target) {
+ return "", fmt.Errorf("result target must be a canonical ULID")
+ }
+ case CommandBatch:
+ if a.Target != "" {
+ return "", fmt.Errorf("batch address must not have a target")
+ }
+
+ return Root + "/devices/" + a.ID + "/commands/$batch", nil
+ case HubStatus:
+ if a.Target != "" {
+ return "", fmt.Errorf("hub address must not have a target")
+ }
+
+ return Root + "/zigbee/hubs/" + a.ID + "/status", nil
+ default:
+ return "", fmt.Errorf("unknown address kind %q", a.Kind)
+ }
+
+ return Root + "/devices/" + a.ID + "/" + string(a.Kind) + "/" + a.Target, nil
+}
+
+// ParseTopic rejects malformed topics, wildcards, and unsupported operations.
+func ParseTopic(topic string) (Address, error) {
+ parts := strings.Split(topic, "/")
+ if len(parts) < 5 || parts[0]+"/"+parts[1] != Root {
+ return Address{}, fmt.Errorf("topic must use the versioned root")
+ }
+
+ var address Address
+
+ switch {
+ case len(parts) == 6 && parts[2] == "zigbee" && parts[3] == "hubs" && parts[5] == "status":
+ address = Address{Kind: HubStatus, ID: parts[4]}
+ case (len(parts) == 5 || len(parts) == 6) && parts[2] == "devices":
+ address = Address{Kind: Kind(parts[4]), ID: parts[3]}
+ if len(parts) == 6 {
+ address.Target = parts[5]
+ if address.Kind == Command && address.Target == "$batch" {
+ address.Kind = CommandBatch
+ address.Target = ""
+ }
+ }
+ default:
+ return Address{}, fmt.Errorf("topic layout is invalid")
+ }
+
+ canonical, err := address.Topic()
+ if err != nil {
+ return Address{}, fmt.Errorf("validate topic address: %w", err)
+ }
+
+ if topic != canonical {
+ return Address{}, fmt.Errorf("topic is not canonical")
+ }
+
+ return address, nil
+}
+
+// Subscription contains a topic filter and the requested delivery level.
+type Subscription struct {
+ Filter string
+ QoS byte
+}
+
+// CoreSubscriptions returns device-to-core filters. Hub diagnostics are not
+// house-state input. Telemetry uses QoS 0. Other subscriptions use QoS 1.
+func CoreSubscriptions() []Subscription {
+ return []Subscription{
+ {Filter: Root + "/devices/+/descriptor", QoS: 1},
+ {Filter: Root + "/devices/+/availability", QoS: 1},
+ {Filter: Root + "/devices/+/reported/+", QoS: 1},
+ {Filter: Root + "/devices/+/telemetry/+", QoS: 0},
+ {Filter: Root + "/devices/+/events/+", QoS: 1},
+ {Filter: Root + "/devices/+/results/+", QoS: 1},
+ }
+}
+
+// CommandSubscription returns the device command filter, including batches.
+func CommandSubscription(deviceID string) (Subscription, error) {
+ if !ValidID(deviceID) {
+ return Subscription{}, fmt.Errorf("device ID must be a canonical ULID")
+ }
+
+ return Subscription{Filter: Root + "/devices/" + deviceID + "/commands/+", QoS: 1}, nil
+}
+
+// HubStatusSubscription returns the filter for all Zigbee hub diagnostics.
+func HubStatusSubscription() Subscription {
+ return Subscription{Filter: Root + "/zigbee/hubs/+/status", QoS: 1}
+}