9 // Root is the versioned MQTT namespace.
12 // Descriptor identifies retained device metadata.
13 Descriptor Kind = "descriptor"
15 // Availability identifies retained device reachability.
16 Availability Kind = "availability"
18 // Reported identifies a retained point value.
19 Reported Kind = "reported"
21 // Telemetry identifies a non-retained measurement.
22 Telemetry Kind = "telemetry"
24 // Event identifies a non-retained occurrence.
27 // Command identifies a non-retained point request.
28 Command Kind = "commands"
30 // CommandBatch identifies an ordered group of point requests.
31 CommandBatch Kind = "batch"
33 // Result identifies a command outcome.
34 Result Kind = "results"
36 // HubStatus identifies retained Zigbee hub diagnostics.
37 HubStatus Kind = "status"
40 // Kind identifies a wire operation, independent of its topic text.
43 // Address identifies a device operation or a Zigbee hub status. Target is a
44 // capability, event, or command ID. Hub status and device metadata omit Target.
51 // Topic validates the address and returns its exact publication topic.
52 func (a Address) Topic() (string, error) {
54 return "", fmt.Errorf("address ID must be a canonical ULID")
58 case Descriptor, Availability:
60 return "", fmt.Errorf("metadata address must not have a target")
63 return Root + "/devices/" + a.ID + "/" + string(a.Kind), nil
64 case Reported, Telemetry, Event, Command:
65 if !ValidIdentifier(a.Target) {
66 return "", fmt.Errorf("point address must have a valid target")
69 if !ValidID(a.Target) {
70 return "", fmt.Errorf("result target must be a canonical ULID")
74 return "", fmt.Errorf("batch address must not have a target")
77 return Root + "/devices/" + a.ID + "/commands/$batch", nil
80 return "", fmt.Errorf("hub address must not have a target")
83 return Root + "/zigbee/hubs/" + a.ID + "/status", nil
85 return "", fmt.Errorf("unknown address kind %q", a.Kind)
88 return Root + "/devices/" + a.ID + "/" + string(a.Kind) + "/" + a.Target, nil
91 // ParseTopic rejects malformed topics, wildcards, and unsupported operations.
92 func ParseTopic(topic string) (Address, error) {
93 parts := strings.Split(topic, "/")
94 if len(parts) < 5 || parts[0]+"/"+parts[1] != Root {
95 return Address{}, fmt.Errorf("topic must use the versioned root")
101 case len(parts) == 6 && parts[2] == "zigbee" && parts[3] == "hubs" && parts[5] == "status":
106 case (len(parts) == 5 || len(parts) == 6) && parts[2] == "devices":
108 Kind: Kind(parts[4]),
112 address.Target = parts[5]
113 if address.Kind == Command && address.Target == "$batch" {
114 address.Kind = CommandBatch
119 return Address{}, fmt.Errorf("topic layout is invalid")
122 canonical, err := address.Topic()
124 return Address{}, fmt.Errorf("validate topic address: %w", err)
127 if topic != canonical {
128 return Address{}, fmt.Errorf("topic is not canonical")
134 // Subscription contains a topic filter and the requested delivery level.
135 type Subscription struct {
140 // CoreSubscriptions returns device-to-core filters. Hub diagnostics are not
141 // house-state input. Telemetry uses QoS 0. Other subscriptions use QoS 1.
142 func CoreSubscriptions() []Subscription {
143 return []Subscription{
145 Filter: Root + "/devices/+/descriptor",
149 Filter: Root + "/devices/+/availability",
153 Filter: Root + "/devices/+/reported/+",
157 Filter: Root + "/devices/+/telemetry/+",
161 Filter: Root + "/devices/+/events/+",
165 Filter: Root + "/devices/+/results/+",
171 // CommandSubscription returns the device command filter, including batches.
172 func CommandSubscription(deviceID string) (Subscription, error) {
173 if !ValidID(deviceID) {
174 return Subscription{}, fmt.Errorf("device ID must be a canonical ULID")
178 Filter: Root + "/devices/" + deviceID + "/commands/+",
183 // HubStatusSubscription returns the filter for all Zigbee hub diagnostics.
184 func HubStatusSubscription() Subscription {
186 Filter: Root + "/zigbee/hubs/+/status",