Skip to main content

ebus_discovery/
advertise.rs

1//! What a host advertises: identity, REST API and brokers, and the records
2//! they become (ebus.py:330-546; _mdns_core.py:900-939, 1062-1063, 1129-1152).
3
4use alloc::format;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8use crate::txt::{TxtError, TxtRecord, check_txt};
9use crate::{
10    BrokerType, DEVICE_INFO_SERVICE, EBUS_SERVICE, EBUS_VERSION, HTTP_SERVICE, HTTPS_SERVICE,
11    ROLE_BROKER_HOST, ROLE_CONTROLLER, ROLE_DEVICE, TXTVERS, strip_dot,
12};
13
14/// Instance-name suffixes tried on a collision: `-2` through `-99`.
15pub const MAX_RENAME_SUFFIX: u32 = 99;
16/// Longest device id used as the default instance name: one 63-octet DNS
17/// label less the `-99` suffix.
18pub const MAX_DEFAULT_INSTANCE_OCTETS: usize = 60;
19
20/// The `homie_version` advertised when [`Identity::homie_version`] is unset.
21const HOMIE_VERSION: &str = "5";
22
23/// MQTT protocol versions the specification names for the broker `protocol` key.
24const MQTT_PROTOCOLS: [&str; 2] = ["mqtt-v5", "mqtt-v3.1.1"];
25
26/// Add `key=value` when `value` is not empty.
27fn put(txt: &mut TxtRecord, key: &str, value: Option<&str>) {
28    if let Some(v) = value.filter(|v| !v.is_empty()) {
29        txt.insert(key, v);
30    }
31}
32
33fn add_extras(txt: &mut TxtRecord, extras: &[(String, String)]) {
34    for (k, v) in extras {
35        txt.insert(k, v);
36    }
37}
38
39/// An entity's `_ebus._tcp` and `_device-info._tcp` content (ebus.py:330-438).
40///
41/// One `_ebus._tcp` advertisement covers the whole host, so `device_ids` may
42/// list several devices, joined with commas into `device_id`. Recommended
43/// keys left empty are omitted. Extra keys never replace a key the identity
44/// sets.
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct Identity {
48    /// The host's device ids; the first is the default instance name.
49    pub device_ids: Vec<String>,
50    /// `device`, `controller`, `broker-host`.
51    pub roles: Vec<String>,
52    /// Required.
53    pub manufacturer: String,
54    /// Required.
55    pub model: String,
56    /// Required, in `_device-info._tcp`.
57    pub serial_number: String,
58    /// Defaults to [`EBUS_VERSION`].
59    pub ebus_version: String,
60    /// `device_type`.
61    pub device_type: Option<String>,
62    /// `name`.
63    pub name: Option<String>,
64    /// `fw_version`, in both records.
65    pub fw_version: Option<String>,
66    /// `register`: the registration endpoint path.
67    pub register: Option<String>,
68    /// `broker_ca`: the broker CA download path.
69    pub broker_ca: Option<String>,
70    /// `auth_methods`.
71    pub auth_methods: Vec<String>,
72    /// `hw_version`, in `_device-info._tcp`.
73    pub hw_version: Option<String>,
74    /// `os_version`, in `_device-info._tcp`.
75    pub os_version: Option<String>,
76    /// `mac`, in `_device-info._tcp`.
77    pub mac: Option<String>,
78    /// Keys the specification does not define, appended to `_ebus._tcp`.
79    pub extra_ebus_txt: Vec<(String, String)>,
80    /// Keys the specification does not define, appended to `_device-info._tcp`.
81    pub extra_device_info_txt: Vec<(String, String)>,
82    /// The Homie domain the host publishes under; adds `homie_domain`,
83    /// `homie_version` and `homie_roles` to `_ebus._tcp`.
84    pub homie_domain: Option<String>,
85    /// `homie_version` with `homie_domain`; defaults to `5`.
86    pub homie_version: Option<String>,
87}
88
89impl Identity {
90    /// An identity with the required fields; every other field empty.
91    pub fn new<D, R>(
92        device_ids: impl IntoIterator<Item = D>,
93        roles: impl IntoIterator<Item = R>,
94        manufacturer: &str,
95        model: &str,
96        serial_number: &str,
97    ) -> Self
98    where
99        D: Into<String>,
100        R: Into<String>,
101    {
102        Self {
103            device_ids: device_ids.into_iter().map(Into::into).collect(),
104            roles: roles.into_iter().map(Into::into).collect(),
105            manufacturer: manufacturer.into(),
106            model: model.into(),
107            serial_number: serial_number.into(),
108            ebus_version: EBUS_VERSION.into(),
109            device_type: None,
110            name: None,
111            fw_version: None,
112            register: None,
113            broker_ca: None,
114            auth_methods: Vec::new(),
115            hw_version: None,
116            os_version: None,
117            mac: None,
118            extra_ebus_txt: Vec::new(),
119            extra_device_info_txt: Vec::new(),
120            homie_domain: None,
121            homie_version: None,
122        }
123    }
124
125    fn trimmed(list: &[String]) -> impl Iterator<Item = &str> {
126        list.iter().map(|s| s.trim())
127    }
128
129    /// The `device_id` value: the device ids joined with commas.
130    pub fn device_id(&self) -> String {
131        Self::trimmed(&self.device_ids)
132            .collect::<Vec<_>>()
133            .join(",")
134    }
135
136    /// Check the required fields and both records (ebus.py:371-393).
137    pub fn check(&self) -> Result<(), TxtError> {
138        if self.device_ids.is_empty() {
139            return Err(TxtError::NoDeviceId);
140        }
141        if let Some(bad) = Self::trimmed(&self.device_ids).find(|d| d.is_empty() || d.contains(','))
142        {
143            return Err(TxtError::InvalidDeviceId(bad.to_string()));
144        }
145        if self.roles.is_empty() {
146            return Err(TxtError::NoRole);
147        }
148        if let Some(bad) = Self::trimmed(&self.roles).find(|r| r.is_empty() || r.contains(',')) {
149            return Err(TxtError::InvalidRole(bad.to_string()));
150        }
151        if let Some(d) = &self.homie_domain {
152            if d.contains(['/', '+', '#']) {
153                return Err(TxtError::InvalidHomieDomain(d.clone()));
154            }
155        }
156        for (field, value) in [
157            ("manufacturer", &self.manufacturer),
158            ("model", &self.model),
159            ("serial_number", &self.serial_number),
160            ("ebus_version", &self.ebus_version),
161        ] {
162            if value.is_empty() {
163                return Err(TxtError::Missing(field));
164            }
165        }
166        check_txt(EBUS_SERVICE, &self.ebus_txt())?;
167        check_txt(DEVICE_INFO_SERVICE, &self.device_info_txt())?;
168        Ok(())
169    }
170
171    /// The `_ebus._tcp` record, keys in python's order (ebus.py:400-422).
172    pub fn ebus_txt(&self) -> TxtRecord {
173        let mut txt = TxtRecord::new();
174        txt.insert("txtvers", TXTVERS);
175        txt.insert("ebus_version", &self.ebus_version);
176        let roles: Vec<&str> = Self::trimmed(&self.roles).collect();
177        txt.insert("roles", &roles.join(","));
178        txt.insert("device_id", &self.device_id());
179        put(&mut txt, "device_type", self.device_type.as_deref());
180        put(&mut txt, "name", self.name.as_deref());
181        put(&mut txt, "manufacturer", Some(&self.manufacturer));
182        put(&mut txt, "model", Some(&self.model));
183        put(&mut txt, "fw_version", self.fw_version.as_deref());
184        put(&mut txt, "register", self.register.as_deref());
185        put(&mut txt, "broker_ca", self.broker_ca.as_deref());
186        let auth: Vec<&str> = Self::trimmed(&self.auth_methods).collect();
187        put(&mut txt, "auth_methods", Some(&auth.join(",")));
188        if let Some(domain) = self.homie_domain.as_deref().filter(|d| !d.is_empty()) {
189            txt.insert("homie_domain", domain);
190            let version = self.homie_version.as_deref().filter(|v| !v.is_empty());
191            txt.insert("homie_version", version.unwrap_or(HOMIE_VERSION));
192            let homie: Vec<&str> = roles
193                .iter()
194                .copied()
195                .filter(|r| *r == ROLE_DEVICE || *r == ROLE_CONTROLLER)
196                .collect();
197            put(&mut txt, "homie_roles", Some(&homie.join(",")));
198        }
199        add_extras(&mut txt, &self.extra_ebus_txt);
200        txt
201    }
202
203    /// The `_device-info._tcp` record (ebus.py:424-438).
204    pub fn device_info_txt(&self) -> TxtRecord {
205        let mut txt = TxtRecord::new();
206        txt.insert("txtvers", TXTVERS);
207        txt.insert("manufacturer", &self.manufacturer);
208        txt.insert("model", &self.model);
209        txt.insert("serial_number", &self.serial_number);
210        put(&mut txt, "fw_version", self.fw_version.as_deref());
211        put(&mut txt, "hw_version", self.hw_version.as_deref());
212        put(&mut txt, "os_version", self.os_version.as_deref());
213        put(&mut txt, "mac", self.mac.as_deref());
214        add_extras(&mut txt, &self.extra_device_info_txt);
215        txt
216    }
217}
218
219/// A REST API advertised as `_http._tcp` or `_https._tcp` (ebus.py:445-477).
220#[derive(Clone, Debug, PartialEq, Eq)]
221#[non_exhaustive]
222pub struct HttpService {
223    /// `_https._tcp` when true.
224    pub tls: bool,
225    /// The API's port.
226    pub port: u16,
227    /// `path`: the API base path.
228    pub path: String,
229    /// `version`: the API version, `1.0` by default.
230    pub version: String,
231    /// `openapi`: the OpenAPI document's path.
232    pub openapi: Option<String>,
233    /// Keys the specification does not define.
234    pub extra_txt: Vec<(String, String)>,
235}
236
237impl HttpService {
238    /// A plain HTTP API at `port` under `path`.
239    pub fn http(port: u16, path: &str) -> Self {
240        Self {
241            tls: false,
242            port,
243            path: path.into(),
244            version: "1.0".into(),
245            openapi: None,
246            extra_txt: Vec::new(),
247        }
248    }
249
250    /// An HTTPS API at `port` under `path`.
251    pub fn https(port: u16, path: &str) -> Self {
252        Self {
253            tls: true,
254            ..Self::http(port, path)
255        }
256    }
257
258    /// `_https._tcp` or `_http._tcp`.
259    pub fn service_type(&self) -> &'static str {
260        if self.tls {
261            HTTPS_SERVICE
262        } else {
263            HTTP_SERVICE
264        }
265    }
266
267    /// The TXT record, carrying the identity's `device_id` and `device_type`.
268    pub fn txt(&self, identity: &Identity) -> TxtRecord {
269        let mut txt = TxtRecord::new();
270        txt.insert("txtvers", TXTVERS);
271        txt.insert("path", &self.path);
272        txt.insert("version", &self.version);
273        txt.insert("device_id", &identity.device_id());
274        put(&mut txt, "device_type", identity.device_type.as_deref());
275        put(&mut txt, "openapi", self.openapi.as_deref());
276        add_extras(&mut txt, &self.extra_txt);
277        txt
278    }
279}
280
281/// A broker this host runs (ebus.py:494-546; framework.md:299-326).
282#[derive(Clone, Debug, PartialEq, Eq)]
283#[non_exhaustive]
284pub struct BrokerService {
285    /// The transport.
286    pub broker_type: BrokerType,
287    /// The port.
288    pub port: u16,
289    /// `protocol`: `mqtt-v5` by default, or `mqtt-v3.1.1`; another value is
290    /// kept and logged.
291    pub protocol: String,
292    /// `broker` for `_secure-mqtt._tcp`: the name the broker's certificate is
293    /// issued for; defaults to the SRV target.
294    pub broker: Option<String>,
295    /// `path` for the WebSocket types, `/mqtt` by default.
296    pub path: String,
297    /// `subprotocol` for the WebSocket types, `mqtt` by default.
298    pub subprotocol: String,
299    /// Keys the specification does not define for the type.
300    pub extra_txt: Vec<(String, String)>,
301}
302
303impl BrokerService {
304    /// A broker of `broker_type` at `port`.
305    pub fn new(broker_type: BrokerType, port: u16) -> Self {
306        Self {
307            broker_type,
308            port,
309            protocol: "mqtt-v5".into(),
310            broker: None,
311            path: "/mqtt".into(),
312            subprotocol: "mqtt".into(),
313            extra_txt: Vec::new(),
314        }
315    }
316
317    /// The TXT record for an advertisement whose SRV target is `server`:
318    /// `txtvers` and `protocol`, then `broker` and `device_id` for
319    /// `_secure-mqtt._tcp`, or `path` and `subprotocol` for the WebSocket types.
320    pub fn txt(&self, identity: &Identity, server: &str) -> TxtRecord {
321        let mut txt = TxtRecord::new();
322        txt.insert("txtvers", TXTVERS);
323        txt.insert("protocol", &self.protocol);
324        match self.broker_type {
325            BrokerType::SecureMqtt => {
326                let broker = self.broker.as_deref().filter(|b| !b.is_empty());
327                txt.insert("broker", strip_dot(broker.unwrap_or(server)));
328                txt.insert("device_id", &identity.device_id());
329            }
330            BrokerType::MqttWss | BrokerType::MqttWs => {
331                txt.insert("path", &self.path);
332                txt.insert("subprotocol", &self.subprotocol);
333            }
334            BrokerType::Mqtt => {}
335        }
336        add_extras(&mut txt, &self.extra_txt);
337        txt
338    }
339}
340
341/// One service an advertisement registers.
342#[derive(Clone, Debug, PartialEq, Eq)]
343#[non_exhaustive]
344pub struct ServiceRecord {
345    /// The service type, without a domain.
346    pub service_type: &'static str,
347    /// The port.
348    pub port: u16,
349    /// The TXT record.
350    pub txt: TxtRecord,
351}
352
353/// Everything a host advertises, all under one instance name.
354#[derive(Clone, Debug, PartialEq, Eq)]
355#[non_exhaustive]
356pub struct Advertisement {
357    /// The identity.
358    pub identity: Identity,
359    /// The instance name; `None` for the default rule ([`Advertisement::instance_names`]).
360    pub instance: Option<String>,
361    /// The `_ebus._tcp` port; `None` for the first REST API's port, else 0.
362    pub ebus_port: Option<u16>,
363    /// The `_device-info._tcp` port, 0 by default.
364    pub device_info_port: u16,
365    /// REST APIs.
366    pub http: Vec<HttpService>,
367    /// Brokers, at most one per type.
368    pub brokers: Vec<BrokerService>,
369}
370
371impl Advertisement {
372    /// The identity's `_ebus._tcp` and `_device-info._tcp` records alone.
373    pub fn new(identity: Identity) -> Self {
374        Self {
375            identity,
376            instance: None,
377            ebus_port: None,
378            device_info_port: 0,
379            http: Vec::new(),
380            brokers: Vec::new(),
381        }
382    }
383
384    /// With an explicit instance name.
385    pub fn instance(mut self, name: &str) -> Self {
386        self.instance = Some(name.into());
387        self
388    }
389
390    /// With a REST API added.
391    pub fn http(mut self, http: HttpService) -> Self {
392        self.http.push(http);
393        self
394    }
395
396    /// With a broker added.
397    pub fn broker(mut self, broker: BrokerService) -> Self {
398        self.brokers.push(broker);
399        self
400    }
401
402    /// Every record, in python's order (_mdns_core.py:900-939): `_ebus._tcp`,
403    /// `_device-info._tcp`, the REST APIs, then the brokers. `server` is the
404    /// SRV target, the default TXT `broker`. Every record is checked with
405    /// [`check_txt`]; brokers without the `broker-host` role are logged with
406    /// `reason=brokersWithoutBrokerHostRole` and `roles` is left as given.
407    pub fn records(&self, server: &str) -> Result<Vec<ServiceRecord>, TxtError> {
408        let id = &self.identity;
409        id.check()?;
410        for role in &id.roles {
411            let role = role.trim();
412            if ![ROLE_DEVICE, ROLE_CONTROLLER, ROLE_BROKER_HOST].contains(&role) {
413                crate::log::warn!("unknownEbusRole", role = role);
414            }
415        }
416        for (i, b) in self.brokers.iter().enumerate() {
417            if self.brokers[..i]
418                .iter()
419                .any(|o| o.broker_type == b.broker_type)
420            {
421                return Err(TxtError::DuplicateBrokerType(b.broker_type));
422            }
423            if !MQTT_PROTOCOLS.contains(&b.protocol.as_str()) {
424                crate::log::warn!("unknownMqttProtocol", protocol = b.protocol);
425            }
426        }
427        if !self.brokers.is_empty() && !id.roles.iter().any(|r| r.trim() == ROLE_BROKER_HOST) {
428            crate::log::warn!("brokersWithoutBrokerHostRole", roles = id.roles.join(","));
429        }
430        let ebus_port = self
431            .ebus_port
432            .or_else(|| self.http.first().map(|h| h.port))
433            .unwrap_or(0);
434        let mut out = Vec::with_capacity(2 + self.http.len() + self.brokers.len());
435        out.push(ServiceRecord {
436            service_type: EBUS_SERVICE,
437            port: ebus_port,
438            txt: id.ebus_txt(),
439        });
440        out.push(ServiceRecord {
441            service_type: DEVICE_INFO_SERVICE,
442            port: self.device_info_port,
443            txt: id.device_info_txt(),
444        });
445        for h in &self.http {
446            out.push(ServiceRecord {
447                service_type: h.service_type(),
448                port: h.port,
449                txt: h.txt(id),
450            });
451        }
452        for b in &self.brokers {
453            out.push(ServiceRecord {
454                service_type: b.broker_type.as_str(),
455                port: b.port,
456                txt: b.txt(id, server),
457            });
458        }
459        for r in &out[2..] {
460            check_txt(r.service_type, &r.txt)?;
461        }
462        Ok(out)
463    }
464
465    /// The instance name all records share: the explicit name, else the first
466    /// device id when it fits [`MAX_DEFAULT_INSTANCE_OCTETS`], else the host
467    /// label of `server`, logged with `reason=deviceIdTooLongForInstanceName`
468    /// (_mdns_core.py:1129-1152).
469    pub fn base_instance_name(&self, server: &str) -> String {
470        if let Some(name) = self.instance.as_deref().filter(|n| !n.is_empty()) {
471            return name.into();
472        }
473        let first = self.identity.device_ids.first().map_or("", |d| d.trim());
474        if first.len() <= MAX_DEFAULT_INSTANCE_OCTETS {
475            return first.into();
476        }
477        let host = strip_dot(server);
478        let host = host.rfind(".local").map_or(host, |at| &host[..at]);
479        crate::log::info!(
480            "deviceIdTooLongForInstanceName",
481            deviceId = first,
482            instance = host
483        );
484        host.into()
485    }
486
487    /// The instance names to try in turn: the base name
488    /// ([`Advertisement::base_instance_name`]), then `-2` through `-99`
489    /// (_mdns_core.py:937-939).
490    pub fn instance_names(&self, server: &str) -> impl Iterator<Item = String> + use<> {
491        let base = self.base_instance_name(server);
492        core::iter::once(base.clone())
493            .chain((2..=MAX_RENAME_SUFFIX).map(move |n| format!("{base}-{n}")))
494    }
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    fn identity() -> Identity {
502        Identity::new(
503            ["panel-1", "meter-2"],
504            ["device", "broker-host"],
505            "Acme",
506            "P1",
507            "SN1",
508        )
509    }
510
511    fn pairs(txt: &TxtRecord) -> Vec<(String, String)> {
512        txt.iter()
513            .map(|(k, _)| (k.to_string(), txt.get(k).unwrap().into_owned()))
514            .collect()
515    }
516
517    fn p(list: &[(&str, &str)]) -> Vec<(String, String)> {
518        list.iter()
519            .map(|(k, v)| (k.to_string(), v.to_string()))
520            .collect()
521    }
522
523    #[test]
524    fn ebus_record() {
525        let mut id = identity();
526        id.auth_methods = alloc::vec!["passphrase".into(), " mtls ".into()];
527        id.homie_domain = Some("ebus".into());
528        id.extra_ebus_txt = p(&[("roles", "x"), ("custom", "1")]);
529        assert_eq!(
530            pairs(&id.ebus_txt()),
531            p(&[
532                ("txtvers", "1"),
533                ("ebus_version", EBUS_VERSION),
534                ("roles", "device,broker-host"),
535                ("device_id", "panel-1,meter-2"),
536                ("manufacturer", "Acme"),
537                ("model", "P1"),
538                ("auth_methods", "passphrase,mtls"),
539                ("homie_domain", "ebus"),
540                ("homie_version", "5"),
541                ("homie_roles", "device"),
542                ("custom", "1"),
543            ])
544        );
545        assert_eq!(
546            pairs(&id.device_info_txt()),
547            p(&[
548                ("txtvers", "1"),
549                ("manufacturer", "Acme"),
550                ("model", "P1"),
551                ("serial_number", "SN1")
552            ])
553        );
554    }
555
556    #[test]
557    fn identity_checks() {
558        let mut id = identity();
559        id.device_ids.clear();
560        assert_eq!(id.check(), Err(TxtError::NoDeviceId));
561        let mut id = identity();
562        id.device_ids.push("a,b".into());
563        assert_eq!(id.check(), Err(TxtError::InvalidDeviceId("a,b".into())));
564        let mut id = identity();
565        id.roles = alloc::vec![" ".into()];
566        assert_eq!(id.check(), Err(TxtError::InvalidRole("".into())));
567        let mut id = identity();
568        id.model.clear();
569        assert_eq!(id.check(), Err(TxtError::Missing("model")));
570        let mut id = identity();
571        id.homie_domain = Some("a/b".into());
572        assert!(matches!(id.check(), Err(TxtError::InvalidHomieDomain(_))));
573        let mut id = identity();
574        id.name = Some("n".repeat(260));
575        assert!(matches!(id.check(), Err(TxtError::StringTooLong { .. })));
576    }
577
578    #[test]
579    fn broker_records() {
580        let id = identity();
581        let secure = BrokerService::new(BrokerType::SecureMqtt, 8883);
582        assert_eq!(
583            pairs(&secure.txt(&id, "host.local.")),
584            p(&[
585                ("txtvers", "1"),
586                ("protocol", "mqtt-v5"),
587                ("broker", "host.local"),
588                ("device_id", "panel-1,meter-2")
589            ])
590        );
591        let mut ws = BrokerService::new(BrokerType::MqttWs, 9001);
592        ws.protocol = "mqtt-v3.1.1".into();
593        assert_eq!(
594            pairs(&ws.txt(&id, "host.local.")),
595            p(&[
596                ("txtvers", "1"),
597                ("protocol", "mqtt-v3.1.1"),
598                ("path", "/mqtt"),
599                ("subprotocol", "mqtt")
600            ])
601        );
602        let plain = BrokerService::new(BrokerType::Mqtt, 1883);
603        assert_eq!(
604            pairs(&plain.txt(&id, "h.local.")),
605            p(&[("txtvers", "1"), ("protocol", "mqtt-v5")])
606        );
607    }
608
609    #[test]
610    fn records_order_and_ports() {
611        let ad = Advertisement::new(identity())
612            .http(HttpService::https(443, "/api/v1"))
613            .broker(BrokerService::new(BrokerType::SecureMqtt, 8883))
614            .broker(BrokerService::new(BrokerType::Mqtt, 1883));
615        let r = ad.records("host.local.").unwrap();
616        let types: Vec<(&str, u16)> = r.iter().map(|r| (r.service_type, r.port)).collect();
617        assert_eq!(
618            types,
619            [
620                ("_ebus._tcp", 443),
621                ("_device-info._tcp", 0),
622                ("_https._tcp", 443),
623                ("_secure-mqtt._tcp", 8883),
624                ("_mqtt._tcp", 1883)
625            ]
626        );
627        let dup = ad
628            .clone()
629            .broker(BrokerService::new(BrokerType::Mqtt, 1884));
630        assert_eq!(
631            dup.records("h."),
632            Err(TxtError::DuplicateBrokerType(BrokerType::Mqtt))
633        );
634    }
635
636    #[test]
637    fn brokers_without_broker_host_role_keep_roles() {
638        let id = Identity::new(["d"], ["device"], "A", "M", "S");
639        let ad = Advertisement::new(id).broker(BrokerService::new(BrokerType::Mqtt, 1883));
640        let r = ad.records("h.local.").unwrap();
641        assert_eq!(r[0].txt.get("roles").as_deref(), Some("device"));
642    }
643
644    #[test]
645    fn instance_names() {
646        let ad = Advertisement::new(identity());
647        let names: Vec<String> = ad.instance_names("host.local.").collect();
648        assert_eq!(names.len(), 99);
649        assert_eq!(names[0], "panel-1");
650        assert_eq!(names[1], "panel-1-2");
651        assert_eq!(names[98], "panel-1-99");
652        assert_eq!(ad.clone().instance("Main").base_instance_name("h."), "Main");
653        let long = "d".repeat(61);
654        let ad = Advertisement::new(Identity::new([long.as_str()], ["device"], "A", "M", "S"));
655        assert_eq!(ad.base_instance_name("box.local."), "box");
656        assert_eq!(ad.base_instance_name("box.example.com"), "box.example.com");
657        let fits = "d".repeat(60);
658        let ad = Advertisement::new(Identity::new([fits.as_str()], ["device"], "A", "M", "S"));
659        assert_eq!(ad.base_instance_name("box.local."), fits);
660    }
661}