xref: /openbmc/entity-manager/src/entity_manager/entity_manager.cpp (revision fd977ec6188e8837a4bcf0528cf2b456d70d22d9)
1 // SPDX-License-Identifier: Apache-2.0
2 // SPDX-FileCopyrightText: Copyright 2018 Intel Corporation
3 
4 #include "entity_manager.hpp"
5 
6 #include "../utils.hpp"
7 #include "../variant_visitors.hpp"
8 #include "configuration.hpp"
9 #include "dbus_interface.hpp"
10 #include "log_device_inventory.hpp"
11 #include "overlay.hpp"
12 #include "perform_scan.hpp"
13 #include "topology.hpp"
14 #include "utils.hpp"
15 
16 #include <boost/asio/io_context.hpp>
17 #include <boost/asio/post.hpp>
18 #include <boost/asio/steady_timer.hpp>
19 #include <boost/container/flat_map.hpp>
20 #include <boost/container/flat_set.hpp>
21 #include <boost/range/iterator_range.hpp>
22 #include <nlohmann/json.hpp>
23 #include <phosphor-logging/lg2.hpp>
24 #include <sdbusplus/asio/connection.hpp>
25 #include <sdbusplus/asio/object_server.hpp>
26 
27 #include <filesystem>
28 #include <fstream>
29 #include <functional>
30 #include <map>
31 #include <regex>
32 constexpr const char* tempConfigDir = "/tmp/configuration/";
33 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
34 
35 static constexpr std::array<const char*, 6> settableInterfaces = {
36     "FanProfile", "Pid", "Pid.Zone", "Stepwise", "Thresholds", "Polling"};
37 
38 const std::regex illegalDbusPathRegex("[^A-Za-z0-9_.]");
39 const std::regex illegalDbusMemberRegex("[^A-Za-z0-9_]");
40 
getPermission(const std::string & interface)41 sdbusplus::asio::PropertyPermission getPermission(const std::string& interface)
42 {
43     return std::find(settableInterfaces.begin(), settableInterfaces.end(),
44                      interface) != settableInterfaces.end()
45                ? sdbusplus::asio::PropertyPermission::readWrite
46                : sdbusplus::asio::PropertyPermission::readOnly;
47 }
48 
EntityManager(std::shared_ptr<sdbusplus::asio::connection> & systemBus,boost::asio::io_context & io)49 EntityManager::EntityManager(
50     std::shared_ptr<sdbusplus::asio::connection>& systemBus,
51     boost::asio::io_context& io) :
52     systemBus(systemBus),
53     objServer(sdbusplus::asio::object_server(systemBus, /*skipManager=*/true)),
54     lastJson(nlohmann::json::object()),
55     systemConfiguration(nlohmann::json::object()), io(io),
56     dbus_interface(io, objServer), powerStatus(*systemBus),
57     propertiesChangedTimer(io)
58 {
59     // All other objects that EntityManager currently support are under the
60     // inventory subtree.
61     // See the discussion at
62     // https://discord.com/channels/775381525260664832/1018929092009144380
63     objServer.add_manager("/xyz/openbmc_project/inventory");
64 
65     entityIface = objServer.add_interface("/xyz/openbmc_project/EntityManager",
66                                           "xyz.openbmc_project.EntityManager");
67     entityIface->register_method("ReScan", [this]() {
68         propertiesChangedCallback();
69     });
70     dbus_interface::tryIfaceInitialize(entityIface);
71 
72     initFilters(configuration.probeInterfaces);
73 }
74 
postToDbus(const nlohmann::json & newConfiguration)75 void EntityManager::postToDbus(const nlohmann::json& newConfiguration)
76 {
77     std::map<std::string, std::string> newBoards; // path -> name
78 
79     // iterate through boards
80     for (const auto& [boardId, boardConfig] : newConfiguration.items())
81     {
82         postBoardToDBus(boardId, boardConfig, newBoards);
83     }
84 
85     for (const auto& [assocPath, assocPropValue] :
86          topology.getAssocs(std::views::keys(newBoards)))
87     {
88         auto findBoard = newBoards.find(assocPath);
89         if (findBoard == newBoards.end())
90         {
91             continue;
92         }
93 
94         auto ifacePtr = dbus_interface.createInterface(
95             assocPath, "xyz.openbmc_project.Association.Definitions",
96             findBoard->second);
97 
98         ifacePtr->register_property("Associations", assocPropValue);
99         dbus_interface::tryIfaceInitialize(ifacePtr);
100     }
101 }
102 
postBoardToDBus(const std::string & boardId,const nlohmann::json & boardConfig,std::map<std::string,std::string> & newBoards)103 void EntityManager::postBoardToDBus(
104     const std::string& boardId, const nlohmann::json& boardConfig,
105     std::map<std::string, std::string>& newBoards)
106 {
107     std::string boardName = boardConfig["Name"];
108     std::string boardNameOrig = boardConfig["Name"];
109     std::string jsonPointerPath = "/" + boardId;
110     // loop through newConfiguration, but use values from system
111     // configuration to be able to modify via dbus later
112     auto boardValues = systemConfiguration[boardId];
113     auto findBoardType = boardValues.find("Type");
114     std::string boardType;
115     if (findBoardType != boardValues.end() &&
116         findBoardType->type() == nlohmann::json::value_t::string)
117     {
118         boardType = findBoardType->get<std::string>();
119         std::regex_replace(boardType.begin(), boardType.begin(),
120                            boardType.end(), illegalDbusMemberRegex, "_");
121     }
122     else
123     {
124         lg2::error("Unable to find type for {BOARD} reverting to Chassis.",
125                    "BOARD", boardName);
126         boardType = "Chassis";
127     }
128 
129     const std::string boardPath =
130         em_utils::buildInventorySystemPath(boardName, boardType);
131 
132     std::shared_ptr<sdbusplus::asio::dbus_interface> inventoryIface =
133         dbus_interface.createInterface(
134             boardPath, "xyz.openbmc_project.Inventory.Item", boardName);
135 
136     std::shared_ptr<sdbusplus::asio::dbus_interface> boardIface =
137         dbus_interface.createInterface(
138             boardPath, "xyz.openbmc_project.Inventory.Item." + boardType,
139             boardNameOrig);
140 
141     dbus_interface.createAddObjectMethod(jsonPointerPath, boardPath,
142                                          systemConfiguration, boardNameOrig);
143 
144     dbus_interface.populateInterfaceFromJson(
145         systemConfiguration, jsonPointerPath, boardIface, boardValues);
146     jsonPointerPath += "/";
147     // iterate through board properties
148     for (const auto& [propName, propValue] : boardValues.items())
149     {
150         if (propValue.type() == nlohmann::json::value_t::object)
151         {
152             std::shared_ptr<sdbusplus::asio::dbus_interface> iface =
153                 dbus_interface.createInterface(boardPath, propName,
154                                                boardNameOrig);
155 
156             dbus_interface.populateInterfaceFromJson(
157                 systemConfiguration, jsonPointerPath + propName, iface,
158                 propValue);
159         }
160     }
161 
162     nlohmann::json::iterator exposes = boardValues.find("Exposes");
163     if (exposes == boardValues.end())
164     {
165         return;
166     }
167     // iterate through exposes
168     jsonPointerPath += "Exposes/";
169 
170     // store the board level pointer so we can modify it on the way down
171     std::string jsonPointerPathBoard = jsonPointerPath;
172     size_t exposesIndex = -1;
173     for (nlohmann::json& item : *exposes)
174     {
175         postExposesRecordsToDBus(item, exposesIndex, boardNameOrig,
176                                  jsonPointerPath, jsonPointerPathBoard,
177                                  boardPath, boardType);
178     }
179 
180     newBoards.emplace(boardPath, boardNameOrig);
181 }
182 
postExposesRecordsToDBus(nlohmann::json & item,size_t & exposesIndex,const std::string & boardNameOrig,std::string jsonPointerPath,const std::string & jsonPointerPathBoard,const std::string & boardPath,const std::string & boardType)183 void EntityManager::postExposesRecordsToDBus(
184     nlohmann::json& item, size_t& exposesIndex,
185     const std::string& boardNameOrig, std::string jsonPointerPath,
186     const std::string& jsonPointerPathBoard, const std::string& boardPath,
187     const std::string& boardType)
188 {
189     exposesIndex++;
190     jsonPointerPath = jsonPointerPathBoard;
191     jsonPointerPath += std::to_string(exposesIndex);
192 
193     auto findName = item.find("Name");
194     if (findName == item.end())
195     {
196         lg2::error("cannot find name in field {ITEM}", "ITEM", item);
197         return;
198     }
199     auto findStatus = item.find("Status");
200     // if status is not found it is assumed to be status = 'okay'
201     if (findStatus != item.end())
202     {
203         if (*findStatus == "disabled")
204         {
205             return;
206         }
207     }
208     auto findType = item.find("Type");
209     std::string itemType;
210     if (findType != item.end())
211     {
212         itemType = findType->get<std::string>();
213         std::regex_replace(itemType.begin(), itemType.begin(), itemType.end(),
214                            illegalDbusPathRegex, "_");
215     }
216     else
217     {
218         itemType = "unknown";
219     }
220     std::string itemName = findName->get<std::string>();
221     std::regex_replace(itemName.begin(), itemName.begin(), itemName.end(),
222                        illegalDbusMemberRegex, "_");
223     std::string ifacePath = boardPath;
224     ifacePath += "/";
225     ifacePath += itemName;
226 
227     if (itemType == "BMC")
228     {
229         std::shared_ptr<sdbusplus::asio::dbus_interface> bmcIface =
230             dbus_interface.createInterface(
231                 ifacePath, "xyz.openbmc_project.Inventory.Item.Bmc",
232                 boardNameOrig);
233         dbus_interface.populateInterfaceFromJson(
234             systemConfiguration, jsonPointerPath, bmcIface, item,
235             getPermission(itemType));
236     }
237     else if (itemType == "System")
238     {
239         std::shared_ptr<sdbusplus::asio::dbus_interface> systemIface =
240             dbus_interface.createInterface(
241                 ifacePath, "xyz.openbmc_project.Inventory.Item.System",
242                 boardNameOrig);
243         dbus_interface.populateInterfaceFromJson(
244             systemConfiguration, jsonPointerPath, systemIface, item,
245             getPermission(itemType));
246     }
247 
248     for (const auto& [name, config] : item.items())
249     {
250         jsonPointerPath = jsonPointerPathBoard;
251         jsonPointerPath.append(std::to_string(exposesIndex))
252             .append("/")
253             .append(name);
254 
255         if (!postConfigurationRecord(name, config, boardNameOrig, itemType,
256                                      jsonPointerPath, ifacePath))
257         {
258             break;
259         }
260     }
261 
262     std::shared_ptr<sdbusplus::asio::dbus_interface> itemIface =
263         dbus_interface.createInterface(
264             ifacePath, "xyz.openbmc_project.Configuration." + itemType,
265             boardNameOrig);
266 
267     dbus_interface.populateInterfaceFromJson(
268         systemConfiguration, jsonPointerPath, itemIface, item,
269         getPermission(itemType));
270 
271     topology.addBoard(boardPath, boardType, boardNameOrig, item);
272 }
273 
postConfigurationRecord(const std::string & name,nlohmann::json & config,const std::string & boardNameOrig,const std::string & itemType,const std::string & jsonPointerPath,const std::string & ifacePath)274 bool EntityManager::postConfigurationRecord(
275     const std::string& name, nlohmann::json& config,
276     const std::string& boardNameOrig, const std::string& itemType,
277     const std::string& jsonPointerPath, const std::string& ifacePath)
278 {
279     if (config.type() == nlohmann::json::value_t::object)
280     {
281         std::string ifaceName = "xyz.openbmc_project.Configuration.";
282         ifaceName.append(itemType).append(".").append(name);
283 
284         std::shared_ptr<sdbusplus::asio::dbus_interface> objectIface =
285             dbus_interface.createInterface(ifacePath, ifaceName, boardNameOrig);
286 
287         dbus_interface.populateInterfaceFromJson(
288             systemConfiguration, jsonPointerPath, objectIface, config,
289             getPermission(name));
290     }
291     else if (config.type() == nlohmann::json::value_t::array)
292     {
293         size_t index = 0;
294         if (config.empty())
295         {
296             return true;
297         }
298         bool isLegal = true;
299         auto type = config[0].type();
300         if (type != nlohmann::json::value_t::object)
301         {
302             return true;
303         }
304 
305         // verify legal json
306         for (const auto& arrayItem : config)
307         {
308             if (arrayItem.type() != type)
309             {
310                 isLegal = false;
311                 break;
312             }
313         }
314         if (!isLegal)
315         {
316             lg2::error("dbus format error {JSON}", "JSON", config);
317             return false;
318         }
319 
320         for (auto& arrayItem : config)
321         {
322             std::string ifaceName = "xyz.openbmc_project.Configuration.";
323             ifaceName.append(itemType).append(".").append(name);
324             ifaceName.append(std::to_string(index));
325 
326             std::shared_ptr<sdbusplus::asio::dbus_interface> objectIface =
327                 dbus_interface.createInterface(ifacePath, ifaceName,
328                                                boardNameOrig);
329 
330             dbus_interface.populateInterfaceFromJson(
331                 systemConfiguration,
332                 jsonPointerPath + "/" + std::to_string(index), objectIface,
333                 arrayItem, getPermission(name));
334             index++;
335         }
336     }
337 
338     return true;
339 }
340 
deviceRequiresPowerOn(const nlohmann::json & entity)341 static bool deviceRequiresPowerOn(const nlohmann::json& entity)
342 {
343     auto powerState = entity.find("PowerState");
344     if (powerState == entity.end())
345     {
346         return false;
347     }
348 
349     const auto* ptr = powerState->get_ptr<const std::string*>();
350     if (ptr == nullptr)
351     {
352         return false;
353     }
354 
355     return *ptr == "On" || *ptr == "BiosPost";
356 }
357 
pruneDevice(const nlohmann::json & systemConfiguration,const bool powerOff,const bool scannedPowerOff,const std::string & name,const nlohmann::json & device)358 static void pruneDevice(const nlohmann::json& systemConfiguration,
359                         const bool powerOff, const bool scannedPowerOff,
360                         const std::string& name, const nlohmann::json& device)
361 {
362     if (systemConfiguration.contains(name))
363     {
364         return;
365     }
366 
367     if (deviceRequiresPowerOn(device) && (powerOff || scannedPowerOff))
368     {
369         return;
370     }
371 
372     logDeviceRemoved(device);
373 }
374 
startRemovedTimer(boost::asio::steady_timer & timer,nlohmann::json & systemConfiguration)375 void EntityManager::startRemovedTimer(boost::asio::steady_timer& timer,
376                                       nlohmann::json& systemConfiguration)
377 {
378     if (systemConfiguration.empty() || lastJson.empty())
379     {
380         return; // not ready yet
381     }
382     if (scannedPowerOn)
383     {
384         return;
385     }
386 
387     if (!powerStatus.isPowerOn() && scannedPowerOff)
388     {
389         return;
390     }
391 
392     timer.expires_after(std::chrono::seconds(10));
393     timer.async_wait(
394         [&systemConfiguration, this](const boost::system::error_code& ec) {
395             if (ec == boost::asio::error::operation_aborted)
396             {
397                 return;
398             }
399 
400             bool powerOff = !powerStatus.isPowerOn();
401             for (const auto& [name, device] : lastJson.items())
402             {
403                 pruneDevice(systemConfiguration, powerOff, scannedPowerOff,
404                             name, device);
405             }
406 
407             scannedPowerOff = true;
408             if (!powerOff)
409             {
410                 scannedPowerOn = true;
411             }
412         });
413 }
414 
pruneConfiguration(bool powerOff,const std::string & name,const nlohmann::json & device)415 void EntityManager::pruneConfiguration(bool powerOff, const std::string& name,
416                                        const nlohmann::json& device)
417 {
418     if (powerOff && deviceRequiresPowerOn(device))
419     {
420         // power not on yet, don't know if it's there or not
421         return;
422     }
423 
424     auto& ifaces = dbus_interface.getDeviceInterfaces(device);
425     for (auto& iface : ifaces)
426     {
427         auto sharedPtr = iface.lock();
428         if (!!sharedPtr)
429         {
430             objServer.remove_interface(sharedPtr);
431         }
432     }
433 
434     ifaces.clear();
435     systemConfiguration.erase(name);
436     topology.remove(device["Name"].get<std::string>());
437     logDeviceRemoved(device);
438 }
439 
publishNewConfiguration(const size_t & instance,const size_t count,boost::asio::steady_timer & timer,const nlohmann::json newConfiguration)440 void EntityManager::publishNewConfiguration(
441     const size_t& instance, const size_t count,
442     boost::asio::steady_timer& timer, // Gerrit discussion:
443     // https://gerrit.openbmc-project.xyz/c/openbmc/entity-manager/+/52316/6
444     //
445     // Discord discussion:
446     // https://discord.com/channels/775381525260664832/867820390406422538/958048437729910854
447     //
448     // NOLINTNEXTLINE(performance-unnecessary-value-param)
449     const nlohmann::json newConfiguration)
450 {
451     loadOverlays(newConfiguration, io);
452 
453     boost::asio::post(io, [this]() {
454         if (!writeJsonFiles(systemConfiguration))
455         {
456             lg2::error("Error writing json files");
457         }
458     });
459 
460     boost::asio::post(io, [this, &instance, count, &timer, newConfiguration]() {
461         postToDbus(newConfiguration);
462         if (count == instance)
463         {
464             startRemovedTimer(timer, systemConfiguration);
465         }
466     });
467 }
468 
469 // main properties changed entry
propertiesChangedCallback()470 void EntityManager::propertiesChangedCallback()
471 {
472     propertiesChangedInstance++;
473     size_t count = propertiesChangedInstance;
474 
475     propertiesChangedTimer.expires_after(std::chrono::milliseconds(500));
476 
477     // setup an async wait as we normally get flooded with new requests
478     propertiesChangedTimer.async_wait(
479         [this, count](const boost::system::error_code& ec) {
480             if (ec == boost::asio::error::operation_aborted)
481             {
482                 // we were cancelled
483                 return;
484             }
485             if (ec)
486             {
487                 lg2::error("async wait error {ERR}", "ERR", ec.message());
488                 return;
489             }
490 
491             if (propertiesChangedInProgress)
492             {
493                 propertiesChangedCallback();
494                 return;
495             }
496             propertiesChangedInProgress = true;
497 
498             nlohmann::json oldConfiguration = systemConfiguration;
499             auto missingConfigurations = std::make_shared<nlohmann::json>();
500             *missingConfigurations = systemConfiguration;
501 
502             auto perfScan = std::make_shared<scan::PerformScan>(
503                 *this, *missingConfigurations, configuration.configurations, io,
504                 [this, count, oldConfiguration, missingConfigurations]() {
505                     // this is something that since ac has been applied to the
506                     // bmc we saw, and we no longer see it
507                     bool powerOff = !powerStatus.isPowerOn();
508                     for (const auto& [name, device] :
509                          missingConfigurations->items())
510                     {
511                         pruneConfiguration(powerOff, name, device);
512                     }
513                     nlohmann::json newConfiguration = systemConfiguration;
514 
515                     deriveNewConfiguration(oldConfiguration, newConfiguration);
516 
517                     for (const auto& [_, device] : newConfiguration.items())
518                     {
519                         logDeviceAdded(device);
520                     }
521 
522                     propertiesChangedInProgress = false;
523 
524                     boost::asio::post(io, [this, newConfiguration, count] {
525                         publishNewConfiguration(
526                             std::ref(propertiesChangedInstance), count,
527                             std::ref(propertiesChangedTimer), newConfiguration);
528                     });
529                 });
530             perfScan->run();
531         });
532 }
533 
534 // Check if InterfacesAdded payload contains an iface that needs probing.
iaContainsProbeInterface(sdbusplus::message_t & msg,const std::unordered_set<std::string> & probeInterfaces)535 static bool iaContainsProbeInterface(
536     sdbusplus::message_t& msg,
537     const std::unordered_set<std::string>& probeInterfaces)
538 {
539     sdbusplus::message::object_path path;
540     DBusObject interfaces;
541     msg.read(path, interfaces);
542     return std::ranges::any_of(interfaces | std::views::keys,
543                                [&probeInterfaces](const auto& ifaceName) {
544                                    return probeInterfaces.contains(ifaceName);
545                                });
546 }
547 
548 // Check if InterfacesRemoved payload contains an iface that needs probing.
irContainsProbeInterface(sdbusplus::message_t & msg,const std::unordered_set<std::string> & probeInterfaces)549 static bool irContainsProbeInterface(
550     sdbusplus::message_t& msg,
551     const std::unordered_set<std::string>& probeInterfaces)
552 {
553     sdbusplus::message::object_path path;
554     std::vector<std::string> interfaces;
555     msg.read(path, interfaces);
556     return std::ranges::any_of(interfaces,
557                                [&probeInterfaces](const auto& ifaceName) {
558                                    return probeInterfaces.contains(ifaceName);
559                                });
560 }
561 
handleCurrentConfigurationJson()562 void EntityManager::handleCurrentConfigurationJson()
563 {
564     if (EM_CACHE_CONFIGURATION && em_utils::fwVersionIsSame())
565     {
566         if (std::filesystem::is_regular_file(currentConfiguration))
567         {
568             // this file could just be deleted, but it's nice for debug
569             std::filesystem::create_directory(tempConfigDir);
570             std::filesystem::remove(lastConfiguration);
571             std::filesystem::copy(currentConfiguration, lastConfiguration);
572             std::filesystem::remove(currentConfiguration);
573 
574             std::ifstream jsonStream(lastConfiguration);
575             if (jsonStream.good())
576             {
577                 auto data = nlohmann::json::parse(jsonStream, nullptr, false);
578                 if (data.is_discarded())
579                 {
580                     lg2::error("syntax error in {PATH}", "PATH",
581                                lastConfiguration);
582                 }
583                 else
584                 {
585                     lastJson = std::move(data);
586                 }
587             }
588             else
589             {
590                 lg2::error("unable to open {PATH}", "PATH", lastConfiguration);
591             }
592         }
593     }
594     else
595     {
596         // not an error, just logging at this level to make it in the journal
597         std::error_code ec;
598         lg2::error("Clearing previous configuration");
599         std::filesystem::remove(currentConfiguration, ec);
600     }
601 }
602 
registerCallback(const std::string & path)603 void EntityManager::registerCallback(const std::string& path)
604 {
605     if (dbusMatches.contains(path))
606     {
607         return;
608     }
609 
610     lg2::debug("creating PropertiesChanged match on {PATH}", "PATH", path);
611 
612     std::function<void(sdbusplus::message_t & message)> eventHandler =
613         [&](sdbusplus::message_t&) { propertiesChangedCallback(); };
614 
615     sdbusplus::bus::match_t match(
616         static_cast<sdbusplus::bus_t&>(*systemBus),
617         "type='signal',member='PropertiesChanged',path='" + path + "'",
618         eventHandler);
619     dbusMatches.emplace(path, std::move(match));
620 }
621 
622 // We need a poke from DBus for static providers that create all their
623 // objects prior to claiming a well-known name, and thus don't emit any
624 // org.freedesktop.DBus.Properties signals.  Similarly if a process exits
625 // for any reason, expected or otherwise, we'll need a poke to remove
626 // entities from DBus.
initFilters(const std::unordered_set<std::string> & probeInterfaces)627 void EntityManager::initFilters(
628     const std::unordered_set<std::string>& probeInterfaces)
629 {
630     nameOwnerChangedMatch = std::make_unique<sdbusplus::bus::match_t>(
631         static_cast<sdbusplus::bus_t&>(*systemBus),
632         sdbusplus::bus::match::rules::nameOwnerChanged(),
633         [this](sdbusplus::message_t& m) {
634             auto [name, oldOwner,
635                   newOwner] = m.unpack<std::string, std::string, std::string>();
636 
637             if (name.starts_with(':'))
638             {
639                 // We should do nothing with unique-name connections.
640                 return;
641             }
642 
643             propertiesChangedCallback();
644         });
645 
646     // We also need a poke from DBus when new interfaces are created or
647     // destroyed.
648     interfacesAddedMatch = std::make_unique<sdbusplus::bus::match_t>(
649         static_cast<sdbusplus::bus_t&>(*systemBus),
650         sdbusplus::bus::match::rules::interfacesAdded(),
651         [this, probeInterfaces](sdbusplus::message_t& msg) {
652             if (iaContainsProbeInterface(msg, probeInterfaces))
653             {
654                 propertiesChangedCallback();
655             }
656         });
657 
658     interfacesRemovedMatch = std::make_unique<sdbusplus::bus::match_t>(
659         static_cast<sdbusplus::bus_t&>(*systemBus),
660         sdbusplus::bus::match::rules::interfacesRemoved(),
661         [this, probeInterfaces](sdbusplus::message_t& msg) {
662             if (irContainsProbeInterface(msg, probeInterfaces))
663             {
664                 propertiesChangedCallback();
665             }
666         });
667 }
668