9 // Root is the versioned MQTT namespace.
12 // Descriptor identifies retained device metadata.
13 Descriptor Kind = "descriptor"
14 // Availability identifies retained device reachability.
15 Availability Kind = "availability"
16 // Reported identifies a retained point value.
17 Reported Kind = "reported"
18 // Telemetry identifies a non-retained measurement.
19 Telemetry Kind = "telemetry"
20 // Event identifies a non-retained occurrence.
22 // Command identifies a non-retained point request.
23 Command Kind = "commands"
24 // CommandBatch identifies an ordered group of point requests.
25 CommandBatch Kind = "batch"
26 // Result identifies a command outcome.
27 Result Kind = "results"
28 // HubStatus identifies retained Zigbee hub diagnostics.
29 HubStatus Kind = "status"
32 // Kind identifies a wire operation, independent of its topic text.
35 // Address identifies a device operation or a Zigbee hub status. Target is a
36 // capability, event, or command ID. Hub status and device metadata omit Target.
43 // Topic validates the address and returns its exact publication topic.
44 func (a Address) Topic() (string, error) {
46 return "", fmt.Errorf("address ID must be a canonical ULID")
50 case Descriptor, Availability:
52 return "", fmt.Errorf("metadata address must not have a target")
55 return Root + "/devices/" + a.ID + "/" + string(a.Kind), nil
56 case Reported, Telemetry, Event, Command:
57 if !ValidIdentifier(a.Target) {
58 return "", fmt.Errorf("point address must have a valid target")
61 if !ValidID(a.Target) {
62 return "", fmt.Errorf("result target must be a canonical ULID")
66 return "", fmt.Errorf("batch address must not have a target")
69 return Root + "/devices/" + a.ID + "/commands/$batch", nil
72 return "", fmt.Errorf("hub address must not have a target")
75 return Root + "/zigbee/hubs/" + a.ID + "/status", nil
77 return "", fmt.Errorf("unknown address kind %q", a.Kind)
80 return Root + "/devices/" + a.ID + "/" + string(a.Kind) + "/" + a.Target, nil
83 // ParseTopic rejects malformed topics, wildcards, and unsupported operations.
84 func ParseTopic(topic string) (Address, error) {
85 parts := strings.Split(topic, "/")
86 if len(parts) < 5 || parts[0]+"/"+parts[1] != Root {
87 return Address{}, fmt.Errorf("topic must use the versioned root")
93 case len(parts) == 6 && parts[2] == "zigbee" && parts[3] == "hubs" && parts[5] == "status":
94 address = Address{Kind: HubStatus, ID: parts[4]}
95 case (len(parts) == 5 || len(parts) == 6) && parts[2] == "devices":
96 address = Address{Kind: Kind(parts[4]), ID: parts[3]}
98 address.Target = parts[5]
99 if address.Kind == Command && address.Target == "$batch" {
100 address.Kind = CommandBatch
105 return Address{}, fmt.Errorf("topic layout is invalid")
108 canonical, err := address.Topic()
110 return Address{}, fmt.Errorf("validate topic address: %w", err)
113 if topic != canonical {
114 return Address{}, fmt.Errorf("topic is not canonical")
120 // Subscription contains a topic filter and the requested delivery level.
121 type Subscription struct {
126 // CoreSubscriptions returns device-to-core filters. Hub diagnostics are not
127 // house-state input. Telemetry uses QoS 0. Other subscriptions use QoS 1.
128 func CoreSubscriptions() []Subscription {
129 return []Subscription{
130 {Filter: Root + "/devices/+/descriptor", QoS: 1},
131 {Filter: Root + "/devices/+/availability", QoS: 1},
132 {Filter: Root + "/devices/+/reported/+", QoS: 1},
133 {Filter: Root + "/devices/+/telemetry/+", QoS: 0},
134 {Filter: Root + "/devices/+/events/+", QoS: 1},
135 {Filter: Root + "/devices/+/results/+", QoS: 1},
139 // CommandSubscription returns the device command filter, including batches.
140 func CommandSubscription(deviceID string) (Subscription, error) {
141 if !ValidID(deviceID) {
142 return Subscription{}, fmt.Errorf("device ID must be a canonical ULID")
145 return Subscription{Filter: Root + "/devices/" + deviceID + "/commands/+", QoS: 1}, nil
148 // HubStatusSubscription returns the filter for all Zigbee hub diagnostics.
149 func HubStatusSubscription() Subscription {
150 return Subscription{Filter: Root + "/zigbee/hubs/+/status", QoS: 1}