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