php-mqtt/client

Subscribe Wildcard

Shadow505 opened this issue · 8 comments

Hello,

I am currently using this wildcard for subscribing:
zigbee2mqtt/Energy/+/+

This works, but as expected only for devices "categorized" as Energy. I would like to also subscribe to another one. What I tried is this:

zigbee2mqtt/Energy|Temperature/+/+

But this does not seem to work (not reporting any messages). Is there a way to do this?

Thank you!

You cannot pass multiple topic patterns in one subscription, but you can simply subscribe twice (and reuse the callback):

$client = new MqttClient($host, $port);
$client->connect(null, true);

$callback = function (string $topic, string $message, bool $retained) {
    // your logic, e.g. store or process the message
};

$client->subscribe('first/topic', $callback, MqttClient::QOS_AT_MOST_ONCE);
$client->subscribe('second/topic', $callback, MqttClient::QOS_AT_MOST_ONCE);

For some reason I thought this would not work due to the looping. But of course it does because the loop is started afterwards. My bad. Thanks for help and the package! :)

Hey @Namoshek,

sorry I have one more question in this regard that is causing me some headache..

I have an array that contains the topic filters to which I want to subscribe and additional data belonging to that topic that I need for my internal logic. Then I iterate over them and call the mqtt client to subscribe and use $this->subscriberCallback(...) as the parameter for the callback.

Here comes the issue: Inside the callback, I need to access the additional data for the corresponding filter. But the filters are stored in their original form, so for example, $this->myFilters will look like this:

Array

(
    [Energy/+/+] => Array
        (
            [someInternalData] => Array
                (
                    [foo] => bar
                )
        )
        
    [Temperature/+/+/+] => Array
        (
            [someInternalData] => Array
                (
                    [foo] => bar

                )
        )
)

If, for example, the callback is being called for the topic Energy/Foo/Bar, how do I link that back to Energy/+/+ in my array?

I looked through the code to find some options, but can't seem to find something that would be "practical".

For example, I thought about just iterating over the raw filters myself and calling the regexifyTopicFilter() method. That way I could link them back together. But that method is declared private.

There is also no method available that returns the registered subscribers, which would also contain the regexified version of the filter.

I don't want to add some nasty code for this (like using reflection to change the access level) , so maybe you have an idea how to go about this in an elegant way? It would be ideal if the raw filter that was matched would be passed to the callback, but maybe there is another way of doing this, that I am overseeing.

Thank in advance!

Not super memory efficient at large scale (with thousands of subscriptions), but probably the simpliest solution in your case is to use an individual callback per subscription. To avoid having to write the callback per subscription, you can use a factory function to create one for you:

$topics = [
    'first/topic/+' => ['param1' => 'foo', 'param2' => true],
    'second/topic/+/+' => ['param1' => 'bar'],
];

$client = new MqttClient($host, $port);
$client->connect(null, true);

$factory = function ($topicPattern) {
    return function (string $topic, string $message, bool $retained, array $matchedWildcards) use ($topicPattern) {
        // Your logic, e.g. store or process the message.
        // You have access to $topicPattern here.
        // You also have $matchedWildcards, which is the matched arguments:
        //   Topic pattern: client/+/room/#
        //   Topic: client/myclient/room/kitchen/sensor1
        //   Matched wildcards: ['myclient', 'kitchen', 'sensor1']
        $this->processMessage($topicPattern, $matchedWildcards, $message);
    };
};

foreach ($topics as $topicPattern => $configuration) {
    $client->subscribe($topicPattern, $factory($topicPattern), MqttClient::QOS_AT_MOST_ONCE);
}

Sorry, I haven't yet been able to test this. I will do so, tomorrow or the day after and let you know.

Thank you!

Hi @Namoshek,

thanks, this technically works, but is really not very convenient. As I wrote before, I intended to use the First class callable syntax that was introduced with PHP 8.1, hence the $this->subscriberCallback(...) as the callback argument.

Without this I now have to do it like this:

	private function callbackFactory(string $topicPattern): \Closure
	{
		return function (string $topic, string $message, bool $retained, array $matchedWildcards) use ($topicPattern) {
			$this->processMessage($topic, $topicPattern, $message, $retained, $matchedWildcards);
		};
	}

	private function processMessage(string $topic, string $topicPattern, string $message, bool $retained, $matchedWildcards)
	{
		// Processing message here
	}

It would be great if the package could be updated to provide a way to access the filter inside the callback, without having to write a wrapper around the callback. The first class callable syntax makes the code much cleaner, but at the moment it does not seem possible to use it if you need access to the topic filter.

Thanks and best regards!

Even though I don't see a strong need for this, I'm open to a PR. The implementation should be straight forward. But of course it should be covered by some tests. 👍

Closed due to inactivity.